diff --git a/1-js/01-getting-started/1-intro/article.md b/1-js/01-getting-started/1-intro/article.md new file mode 100644 index 00000000..07cbc18a --- /dev/null +++ b/1-js/01-getting-started/1-intro/article.md @@ -0,0 +1,121 @@ +# An introduction to JavaScript + +Let's see what's so special about JavaScript, what we can achieve with it and which other technologies play well with it. + +## What is JavaScript? + +*JavaScript* was initially created to *"make webpages alive"*. + +The programs in this language are called *scripts*. They can be written right in the HTML and execute automatically as the page loads. + +Scripts are provided and executed as a plain text. They don't need a special preparation or a compilation to run. + +In this aspect, JavaScript is very different from another language called [Java](http://en.wikipedia.org/wiki/Java). + +```smart header="Why JavaScript?" +When JavaScript was created, it initially had another name: "LiveScript". But Java language was very popular at that time, so it was decided that positioning a new language as a "younger brother" of Java would help. + +But as it evolved, JavaScript became a fully independent language, with its own specification called [ECMAScript](http://en.wikipedia.org/wiki/ECMAScript), and now it has no relation to Java at all. +``` + +At present, JavaScript can execute not only in the browser, but also on the server, or actually on any device where exists a special program called [the JavaScript engine](https://en.wikipedia.org/wiki/JavaScript_engine). + +The browser has an embedded engine, sometimes it's also called a "JavaScript virtual machine". + +Different engines have different "codenames", for example: + +- [V8](https://en.wikipedia.org/wiki/V8_(JavaScript_engine)) -- in Chrome and Opera. +- [Gecko](https://en.wikipedia.org/wiki/Gecko_(software)) -- in Firefox. +- ...There are other codenames like "Trident", "Chakra" for different versions of IE, "ChakraCore" for Microsoft Edge, "Nitro" and "SquirrelFish" for Safari etc. + +These terms above are good to remember, because they are used in developer articles in the internet. We'll use them too. For instance, if "a feature X is supported by V8", then it probably works in Chrome and Opera. + +```smart header="How the engines work?" + +Engines are complicated. But the basics are easy. + +1. The script is written and distributed as a plain text (can be compressed/optimized by so-called "javascript minifiers"). +2. The engine (embedded if it's a browser) reads the script ("parses") and converts ("compiles") it to the machine language. +3. And then it runs, pretty fast. + +The engine applies optimizations on every stage of the process. It even watches the script as it runs, analyzes the data which flows through it and applies optimizations to the machine-code basing on that knowledge. +``` + +## What can in-browser JavaScript do? + +The modern JavaScript is a "safe" programming language. It does not provide low-level access to memory or CPU, because it was initially created for browsers which do not require it. + +The capabilities greatly depend on the environment which runs JavaScript. For instance, [Node.JS](https://wikipedia.org/wiki/Node.js) supports functions that allows JavaScript to read/write arbitrary files, perform network requests etc. + +In-browser JavaScript can do everything related to webpage manipulation, interaction with the user and the webserver. + +For instance, in-browser JavaScript is able to: + +- Add new HTML to the page, change the existing content, modify styles. +- React on user actions, run on mouse clicks, pointer movements, key presses. +- Send requests over the network to remote servers, download and upload files (so-called [AJAX](https://en.wikipedia.org/wiki/Ajax_(programming)) and [COMET](https://en.wikipedia.org/wiki/Comet_(programming)) technologies). +- Get and set cookies, ask questions to the visitor, show messages. +- Remember the data on the browser side ("local storage"). + +## What can in-browser JavaScript NOT do? + +JavaScript abilities in the browser are limited for the sake of the user's safety. The aim is to prevent an evil webpage from accessing private information or harming the user's data. + +The examples of such restrictions are: + +- JavaScript on the webpage may not read/write arbitrary files on the hard disk, copy them or execute programs. It has no direct access to OS system functions. + + Modern browsers allow it to work with files, but the access is limited and only provided if the user does certain actions, like "dropping" a file into a browser window or selecting it via an `` tag. + + There are ways to interact with camera/microphone and other devices, but they require an explicit user's permission. So a JavaScript-enabled page may not sneakily enable a web-camera, observe the surroundings and send the information to the [NSA](https://en.wikipedia.org/wiki/National_Security_Agency). +- Different tabs/windows generally do not know about each other. Sometimes they do, for example when one window uses JavaScript to open the other one. But even in this case, JavaScript from one page may not access the other if they come from different sites (from a different domain, protocol or port). + + That is called a "Same Origin Policy". To workaround that, *both pages* must contain a special JavaScript code that handles data exchange. + + The limitation is again for user's safety. A page from `http://anysite.com` which a user has opened occasionaly must not be able to open or access another browser tab with the URL `http://gmail.com` and steal information from there. +- JavaScript can easily communicate over the net to the server where the current page came from. But its ability to receive data from other sites/domains is crippled. Though possible, it requires the explicit agreement (expressed in HTTP headers) from the remote side. Once again, that's safety limitations. + +![](limitations.png) + +Such limits do not exist if JavaScript is used outside of the browser, for example on a server. Modern browsers also allow installing plugin/extensions which may get extended permissions. + +## What makes JavaScript unique? + +There are at least *three* great things about JavaScript: + +```compare ++ Full integration with HTML/CSS. ++ Simple things done simply. ++ Supported by all major browsers and enabled by default. +``` + +Combined, these 3 things only exist in JavaScript and no other browser technology. + +That's what makes JavaScript unique. That's why it is the most widespread way of creating browser interfaces. + +While planning to learn a new technology, it's beneficial to check its perspectives. So let's move on to the modern trends that include new languages and browser abilities. + + +## Languages "over" JavaScript + +The syntax of JavaScript does not suit everyone's needs. Different people want different features. + +That's normal, because projects and requirements are different for everyone. + +So recently a plethora of new languages appeared, which are *transpiled* (converted) to JavaScript before they run in the browser. + +The modern tools make the transpilation very fast and transparent, actually allowing developers to code in another language, autoconverting it "under the hood". + +Examples of such languages: + +- [CoffeeScript](http://coffeescript.org/) is a "syntax sugar" for JavaScript, it introduces shorter syntax, allowing to write more precise and clear code. Usually Ruby guys like it. +- [TypeScript](http://www.typescriptlang.org/) is concentrated on adding "strict data typing", to simplify development and support of complex systems. It is developed by Microsoft. +- [Dart](https://www.dartlang.org/) is a standalone language that has its own engine that runs in non-browser environments (like mobile apps). It was initially offered by Google as a replacement for JavaScript, but as of now, browsers require it to be transpiled to JavaScript just like the ones above. + +There are more. Of course even if we use one of those languages, we should also know JavaScript, to really understand what we're doing. + +## Summary + +- JavaScript was initially created as a browser-only language, but now it is used in many other environments as well. +- At this moment, JavaScript has a unique position as a most widely adopted browser language with full integration with HTML/CSS. +- There are many languages that get "transpiled" to JavaScript and provide certain features. It is recommended to take a look at them, at least briefly, after mastering JavaScript. diff --git a/1-js/01-getting-started/1-intro/limitations.png b/1-js/01-getting-started/1-intro/limitations.png new file mode 100644 index 00000000..d5d315c5 Binary files /dev/null and b/1-js/01-getting-started/1-intro/limitations.png differ diff --git a/1-js/01-getting-started/1-intro/limitations@2x.png b/1-js/01-getting-started/1-intro/limitations@2x.png new file mode 100644 index 00000000..01483997 Binary files /dev/null and b/1-js/01-getting-started/1-intro/limitations@2x.png differ diff --git a/1-js/01-getting-started/2-code-editors/article.md b/1-js/01-getting-started/2-code-editors/article.md new file mode 100644 index 00000000..8a835e61 --- /dev/null +++ b/1-js/01-getting-started/2-code-editors/article.md @@ -0,0 +1,62 @@ +# Code editors + +A code editor is the place where a programmer spends most of his time. + +There are two archetypes: IDE and lightweight editors. Many people feel comfortable choosing one tool of each type. + +[cut] + +## IDE + +The term [IDE](https://en.wikipedia.org/wiki/Integrated_development_environment) (Integrated Development Environment) means a powerful editor with many features that usually operates on a "whole project". As said, that's not just an editor, but a full-scale "development environment". + +An IDE loads the project (can be many files), and then allows to navigate between files, provides autocompletion based on the whole project, integrates with version management system (like [git](https://git-scm.com/)), with testing environment and other "project-level" stuff. + +If you haven't considered selecting an IDE, look at the following variants: + +- IntelliJ editors: [WebStorm](http://www.jetbrains.com/webstorm/) for frontend development and [PHPStorm (PHP)](http://www.jetbrains.com/phpstorm/), [IDEA (Java)](http://www.jetbrains.com/idea/), [RubyMine (Ruby)](http://www.jetbrains.com/ruby/) and other if you need additional languages. +- Visual Studio is fine if you're a .NET developer. +- Eclipse-based products, like [Aptana](http://www.aptana.com/) and Zend Studio. +- [Komodo IDE](http://www.activestate.com/komodo-ide) and it's lightweight free version [Komodo Edit](http://www.activestate.com/komodo-edit). +- [Netbeans](http://netbeans.org/). + +All of them with the exception of Visual Studio are cross-platform. Visual Studio is now available for Mac and for Windows (https://www.visualstudio.com/vs/visual-studio-mac/) + +Most IDEs are paid, but have a trial period. Their cost is usually negligible compared to a qualified developer's salary, so just choose the best one for you. + +## Lightweight editors + +"Lightweight editors" are not as powerful as IDEs, but they're fast, elegant and simple. + +They are mainly used to instantly open and edit a file. + +The main difference between a "lightweight editor" and an "IDE" is that IDE works on a project-level, so it loads much more data on start, analyzes the project structure if needed and so on. A lightweight editor is much faster if we need only one file. + +In practice, lightweight editors may have a lot of plugins including directory-level syntax analyzers and autocompleters, so there's no strict border between a lightweight editor and an IDE. + +The following options deserve your attention: + +- [Visual Studio Code](https://code.visualstudio.com/) (cross-platform, free). +- [Atom](https://atom.io/) (cross-platform, free). +- [Sublime Text](http://www.sublimetext.com) (cross-platform, shareware). +- [Notepad++](https://notepad-plus-plus.org/) (Windows, free). +- Vim, Emacs are cool, if you know how to use them. + +## My favorites + +The personal preference of the author is to have both an IDE for projects and a lightweight editor for quick and easy file editing. + +I'm using: + +- [WebStorm](http://www.jetbrains.com/webstorm/) for JS, and if there is one more language in the project, then I switch to other Jetbrains editors like [PHPStorm](http://www.jetbrains.com/phpstorm/) (PHP), [IDEA](http://www.jetbrains.com/idea/) (Java), [RubyMine](http://www.jetbrains.com/ruby/) (Ruby). There are editors for other languages too, but I didn't use them. +- As a lightweight editor -- [Sublime Text](http://www.sublimetext.com) or [Atom](https://atom.io/). + +If you don't know what to choose -- you can consider these ones. + +## Let's not argue + +The editors in the lists above are those that me or my friends -- good developers are using for a long time and are happy with. + +There are other great editors in our big world, please choose the one you like the most. + +The choice of an editor, like any other tool, is individual and depends on your projects, habits, personal preferences. diff --git a/1-js/01-getting-started/3-devtools/article.md b/1-js/01-getting-started/3-devtools/article.md new file mode 100644 index 00000000..544b6175 --- /dev/null +++ b/1-js/01-getting-started/3-devtools/article.md @@ -0,0 +1,60 @@ +# Developer console + +A code is prone to errors. You are quite likely to have errors... Oh, what am I talking about? You are *absolutely* going to make errors, at least if you're a human, not a [robot](https://en.wikipedia.org/wiki/Bender_(Futurama)). + +But in the browser, a user doesn't see the errors by default. So, if something goes wrong in the script, we won't see what's broken and can't fix it. + +To see errors and get a lot of other useful information about scripts, browsers have embedded "developer tools". + +Most often developers lean towards Chrome or Firefox for the development, because those browsers have the best developer tools. Other browsers also provide developer tools, sometimes with special features, but are usually playing "catching-up" to Chrome or Firefox. So most people have a "favorite" browser and switch to others if a problem is browser-specific. + +Developer tools are really powerful, there are many features. To start, we'll learn how to open them, look at errors and run JavaScript commands. + +[cut] + +## Google Chrome + +Open the page [bug.html](bug.html). + +There's an error in the JavaScript code on it. It's hidden from a regular visitor's eyes, so let's open developer tools to see it. + +Press the key `key:F12` or, if you're on Mac, then `key:Cmd+Opt+J`. + +The developer tools will open on the Console tab by default. + +It looks somewhat like this: + +![chrome](chrome.png) + +The exact look of developer tools depends on your version of Chrome. It changes from time to time, but should be similar. + +- Here we can see the red-colored error message. In this case the script contains an unknown "lalala" command. +- On the right, there is a clickable link to the source `bug.html:12` with the line number where the error has occured. + +Below the error message there is a blue `>` symbol. It marks a "command line" where we can type JavaScript commands and press `key:Enter` to run them (`key:Shift+Enter` to input multiline commands). + +Now we can see errors and that's enough for the start. We'll be back to developer tools later and cover debugging more in-depth in the chapter . + + +## Firefox, Edge and others + +Most other browsers use `key:F12` to open developer tools. + +The look & feel of them is quite similar. Once you know how to use one of them (can start with Chrome), you can easily switch to another. + +## Safari + +Safari (Mac browser, not supported for Windows/Linux) is a little bit special here. We need to enable the "Develop menu" first. + +Open Preferences and go to "Advanced" pane. There's a checkbox at the bottom of it: + +![safari](safari.png) + +Now `key:Cmd+Opt+C` can toggle the console. Also note that the new top menu item named "Develop" has appeared. It has many commands and options. + +## Summary + +- Developer tools allow us to see errors, run commands, examine variables and much more. +- They can be opened with `key:F12` for most browsers under Windows. Chrome for Mac needs `key:Cmd+Opt+J`, Safari: `key:Cmd+Opt+C` (need to enable first). + +Now we have the environment ready. In the next section we get down to JavaScript. diff --git a/1-js/01-getting-started/3-devtools/bug.html b/1-js/01-getting-started/3-devtools/bug.html new file mode 100644 index 00000000..edb02375 --- /dev/null +++ b/1-js/01-getting-started/3-devtools/bug.html @@ -0,0 +1,17 @@ + + + + + + + + + + There is an error in the script on this page. + + + + + \ No newline at end of file diff --git a/1-js/01-getting-started/3-devtools/chrome.png b/1-js/01-getting-started/3-devtools/chrome.png new file mode 100644 index 00000000..fd029b74 Binary files /dev/null and b/1-js/01-getting-started/3-devtools/chrome.png differ diff --git a/1-js/01-getting-started/3-devtools/chrome@2x.png b/1-js/01-getting-started/3-devtools/chrome@2x.png new file mode 100644 index 00000000..b87404a8 Binary files /dev/null and b/1-js/01-getting-started/3-devtools/chrome@2x.png differ diff --git a/1-js/01-getting-started/3-devtools/safari.png b/1-js/01-getting-started/3-devtools/safari.png new file mode 100644 index 00000000..37598a26 Binary files /dev/null and b/1-js/01-getting-started/3-devtools/safari.png differ diff --git a/1-js/01-getting-started/3-devtools/safari@2x.png b/1-js/01-getting-started/3-devtools/safari@2x.png new file mode 100644 index 00000000..c59cebef Binary files /dev/null and b/1-js/01-getting-started/3-devtools/safari@2x.png differ diff --git a/1-js/01-getting-started/index.md b/1-js/01-getting-started/index.md new file mode 100644 index 00000000..b327c786 --- /dev/null +++ b/1-js/01-getting-started/index.md @@ -0,0 +1,3 @@ +# An introduction + +About the JavaScript language and the environment to develop with it. diff --git a/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/solution.md b/1-js/02-first-steps/01-hello-world/1-hello-alert/solution.md similarity index 100% rename from 1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/solution.md rename to 1-js/02-first-steps/01-hello-world/1-hello-alert/solution.md diff --git a/1-js/02-first-steps/01-hello-world/1-hello-alert/solution.view/index.html b/1-js/02-first-steps/01-hello-world/1-hello-alert/solution.view/index.html new file mode 100644 index 00000000..45e6744b --- /dev/null +++ b/1-js/02-first-steps/01-hello-world/1-hello-alert/solution.view/index.html @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/1-js/02-first-steps/01-hello-world/1-hello-alert/task.md b/1-js/02-first-steps/01-hello-world/1-hello-alert/task.md new file mode 100644 index 00000000..afed6a91 --- /dev/null +++ b/1-js/02-first-steps/01-hello-world/1-hello-alert/task.md @@ -0,0 +1,12 @@ +importance: 5 + +--- + +# Show an alert + +Create a page that shows a message "I'm JavaScript!". + +Do it in a sandbox, or on your hard drive, doesn't matter, just ensure that it works. + +[demo src="solution"] + diff --git a/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/alert.js b/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/alert.js new file mode 100644 index 00000000..4de72597 --- /dev/null +++ b/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/alert.js @@ -0,0 +1 @@ +alert("I'm JavaScript!"); \ No newline at end of file diff --git a/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/index.html b/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/index.html new file mode 100644 index 00000000..10895f8f --- /dev/null +++ b/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/solution.md b/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/solution.md new file mode 100644 index 00000000..f42c41e6 --- /dev/null +++ b/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/solution.md @@ -0,0 +1,8 @@ +The HTML code: + +[html src="index.html"] + +For the file `alert.js` in the same folder: + +[js src="alert.js"] + diff --git a/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/task.md b/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/task.md new file mode 100644 index 00000000..26168d6a --- /dev/null +++ b/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/task.md @@ -0,0 +1,9 @@ +importance: 5 + +--- + +# Show an alert with an external script + +Take the solution of the previous task . Modify it by extracting the script content into an external file `alert.js`, residing in the same folder. + +Open the page, ensure that the alert works. diff --git a/1-js/02-first-steps/01-hello-world/article.md b/1-js/02-first-steps/01-hello-world/article.md new file mode 100644 index 00000000..631d8bd2 --- /dev/null +++ b/1-js/02-first-steps/01-hello-world/article.md @@ -0,0 +1,137 @@ +# Hello, world! + +The tutorial that you're reading is about core JavaScript, which is platform-independent. Further on, you will learn Node.JS and other platforms that use it. + +But, we need a working environment to run our scripts, and, just because this book is online, the browser is a good choice. We'll keep the amount of browser-specific commands (like `alert`) to a minimum, so that you don't spend time on them if you plan to concentrate on another environment like Node.JS. On the other hand, browser details are explained in detail in the [next part](/ui) of the tutorial. + +So first, let's see how to attach a script to the webpage. For server-side environments, you can just execute it with a command like `"node my.js"` for Node.JS. + + +[cut] + +## The "script" tag + +JavaScript programs can be inserted in any place of HTML with the help of the ` +*/!* + +

...After the script.

+ + + + +``` + +```online +You can run the example clicking on a "Play" button in it's right-top corner. +``` + +The ` + ``` + + These comments were supposed to hide the code from an old browser that did't know about a ` +``` + +Here `/path/to/script.js` is an absolute path to the file with the script (from the site root). + +It is also possible to provide a path relative to the current page. For instance, `src="script.js"` would mean a file `"script.js"` from the current folder. + +We can give a full URL as well, for instance: + +```html + +``` + +To attach several scripts, use multiple tags: + +```html + + +… +``` + +```smart +As a rule, only simplest scripts are put into HTML. More complex ones reside in separate files. + +The benefit of a separate file is that the browser will download it and then store in its [cache](https://en.wikipedia.org/wiki/Web_cache). + +After this, other pages which want the same script will take it from the cache instead of downloading it. So the file is actually downloaded only once. + +That saves traffic and makes pages faster. +``` + +````warn header="If `src` is set, the script content is ignored." +A single ` +``` + +We must choose: either it's an external ` + +``` +```` + +## Summary + +- We can use a ``. + + +There is much more about browser scripts and their interaction with the web-page. But let's keep in mind that this part of the tutorial is devoted to JavaScript language, so we shouldn't distract ourselves from it. We'll be using a browser as a way to run JavaScript, which is very convenient for online reading, but yet one of many. diff --git a/1-js/02-first-steps/01-hello-world/hello-world-render.png b/1-js/02-first-steps/01-hello-world/hello-world-render.png new file mode 100644 index 00000000..ffe81069 Binary files /dev/null and b/1-js/02-first-steps/01-hello-world/hello-world-render.png differ diff --git a/1-js/02-first-steps/01-hello-world/hello-world-render@2x.png b/1-js/02-first-steps/01-hello-world/hello-world-render@2x.png new file mode 100644 index 00000000..c4411027 Binary files /dev/null and b/1-js/02-first-steps/01-hello-world/hello-world-render@2x.png differ diff --git a/1-js/02-first-steps/02-structure/article.md b/1-js/02-first-steps/02-structure/article.md new file mode 100644 index 00000000..3a5aa88e --- /dev/null +++ b/1-js/02-first-steps/02-structure/article.md @@ -0,0 +1,161 @@ +# Code structure + +The first thing to study is the building blocks of the code. + +[cut] + +## Statements + +Statements are syntax constructs and commands that perform actions. + +We've already seen a statement `alert('Hello, world!')`, which shows the message. + +We can have as many statements in the code as we want. Another statement can be separated with a semicolon. + +For example, here we split the message into two: + +```js run no-beautify +alert('Hello'); alert('World'); +``` + +Usually each statement is written on a separate line -- thus the code becomes more readable: + +```js run no-beautify +alert('Hello'); +alert('World'); +``` + +## Semicolons [#semicolon] + +A semicolon may be omitted in most cases when a line break exists. + +This would also work: + +```js run no-beautify +alert('Hello') +alert('World') +``` + +Here JavaScript interprets the line break as an "implicit" semicolon. That's also called an [automatic semicolon insertion](https://tc39.github.io/ecma262/#sec-automatic-semicolon-insertion). + +**In most cases a newline implies a semicolon. But "in most cases" does not mean "always"!** + +There are cases when a newline does not mean a semicolon, for example: + +```js run no-beautify +alert(3 + +1 ++ 2); +``` + +The code outputs `6`, because JavaScript does not insert semicolons here. It is intuitively obvious that if the line ends with a plus `"+"`, then it is an "incomplete expression", no semicolon required. And in this case that works as intended. + +**But there are situations where JavaScript "fails" to assume a semicolon where it is really needed.** + +Errors which occur in such cases are quite hard to find and fix. + +````smart header="An example of an error" +If you're curious to see a concrete example of such an error, check this code out: + +```js run +[1, 2].forEach(alert) +``` + +No need to think about the meaning of the brackets `[]` and `forEach` yet. We'll study them later, for now -- it does not matter. Let's just remember the result: it shows `1`, then `2`. + +Now let's add an `alert` before the code. And *not* finish it with a semicolon: + +```js run no-beautify +alert("There will be an error") + +[1, 2].forEach(alert) +``` + +Now if we run it, only the first `alert` is shown, and then we have an error! + +But everything is fine again if we add a semicolon after `alert`: +```js run +alert("All fine now"); + +[1, 2].forEach(alert) +``` + +Now we have the "All fine now" message and then `1` and `2`. + + +The error in the no-semicolon variant occurs because JavaScript does not imply a semicolon before square brackets `[...]`. + +So, because the semicolon is not auto-inserted, the code in the first example is treated as a single statement, that's how the engine sees it: + +```js run no-beautify +alert("There will be an error")[1, 2].forEach(alert) +``` + +But it should be two separate statements, not a single one. Such a merging in this case is just wrong, hence the error. There are other situations when such thing happens. +```` + +It's recommended to put semicolons between statements even if they are separated by newlines. This rule is widely adopted by the community. Let's note once again -- *it is possible* to leave out semicolons most of time. But it's safer, especially for a beginner -- to put them. + +## Comments + +As the time goes, the program becomes more and more complex. It becomes necessary to add *comments* which describe what happens and why. + +Comments can be put into any place of the script. They don't affect the execution, because the engine simply ignores them. + +**One-line comments start with the two forward slash characters `//`.** + +The rest of the line is a comment. It may occupy a full line of its own or follow a statement. + +Like here: +```js run +// This comment occupies a line of its own +alert('Hello'); + +alert('World'); // This comment follows the statement +``` + +**Multiline comments start with a forward slash and an asterisk "/*" and end with an asterisk and a forward slash "*/".** + +Like this: + +```js run +/* An example with two messages. +This is a multiline comment. +*/ +alert('Hello'); +alert('World'); +``` + +The content of comments is ignored, so if we put a code inside /* ... */ it won't execute. + +Sometimes it comes handy to temporarily disable a part of the code: + +```js run +/* Commenting out the code +alert('Hello'); +*/ +alert('World'); +``` + +```smart header="Use hotkeys!" +In most editors a line of code can be commented out by `key:Ctrl+/` hotkey for a single-line comment and something like `key:Ctrl+Shift+/` -- for multiline comments (select a code and press the hotkey). For Mac try `key:Cmd` instead of `key:Ctrl`. +``` + +````warn header="Nested comments are not supported!" +There may not be `/*...*/` inside another `/*...*/`. + +This code will die with an error: + +```js run no-beautify +/* + /* nested comment ?!? */ +*/ +alert( 'World' ); +``` +```` + +Please, don't hesitate to comment your code. + +Comments increase the overall code footprint, but that's not a problem at all. There are many tools which minify the code before publishing to production server. They remove comments, so comments do not appear in the working scripts. So, the comments do not have any negative effects on production at all. + +Further in the tutorial, there will be a chapter that also explains how to write better comments. diff --git a/1-js/02-first-steps/03-strict-mode/article.md b/1-js/02-first-steps/03-strict-mode/article.md new file mode 100644 index 00000000..e70124c6 --- /dev/null +++ b/1-js/02-first-steps/03-strict-mode/article.md @@ -0,0 +1,70 @@ +# The modern mode, "use strict" + +For a long time JavaScript was evolving without compatibility issues. New features were added to the language, but the old functionality did not change. + +That had the benefit of never breaking the existing codes. But the downside was that any mistake or an imperfect decision made by JavaScript creators got stuck in the language forever. + +It had been so until 2009 when ECMAScript 5 (ES5) appeared. It added new features to the language and modified some of the existing ones. To keep the old code working, most modifications are off by default. One needs to enable them explicitly with a special directive `"use strict"`. + + +[cut] + +## "use strict" + +The directive looks like a string: `"use strict"` or `'use strict'`. When it is located on the top of the script, then the whole script works the "modern" way. + +For example + +```js +"use strict"; + +// this code works the modern way +... +``` + +```warn header="There's no way to cancel `use strict`" +There is no directive `"no use strict"` or alike, that would return the old behavior. + +Once we enter the strict mode, there's no return. +``` + +````warn header="Ensure that 'use strict' is at the top" +Please make sure that `"use strict"` is on the top of the script, otherwise the strict mode may not be enabled. + +There is no strict mode here: + +```js no-strict +alert("some code"); +// "use strict" below is ignored, must be on the top + +"use strict"; + +// strict mode is not activated +``` + +Only comments may appear above `"use strict"`. +```` + + +```smart header="`use strict` for functions" +We will learn functions (a way to group commands) soon. + +Looking ahead let's just note that `"use strict"` can be put at the start of a function (most kinds of functions) instead of the whole script. Then strict mode is enabled in that function only. But usually people use it for the whole script. +``` + + +## Start with "use strict" + +It is recommended to always start a script with `"use strict"`, for the following reasons: + +1. First, all modern browsers support it. Only outdated ones like Internet Explorer 9 and below do not. +2. Second, the modern JavaScript actually forces us into the strict mode. There are several modern language features like "classes" and "modules" that enable strict mode automatically. So, it's hard to evade it. + +Here in the tutorial, all code (where not explicitly noted otherwise) works in `"use strict"`. We concentrate on modern JavaScript. But there will be notes about what happens without `"use strict"`, so that you can understand what's going on if you forget it or if you're working with an outdated script that doesn't have it. + +## Summary + +- The `"use strict"` directive switches the engine to the "modern" mode, changing the behavior of some built-in features. +- Several modern features of the language enable `"use strict"` implicitly, so it's quite hard to evade it. + +It's always recommended to start scripts with `"use strict"`. All examples in this book assume so, unless (very rarely) specified otherwise. diff --git a/1-js/02-first-steps/04-variables/1-hello-variables/solution.md b/1-js/02-first-steps/04-variables/1-hello-variables/solution.md new file mode 100644 index 00000000..9249e1c8 --- /dev/null +++ b/1-js/02-first-steps/04-variables/1-hello-variables/solution.md @@ -0,0 +1,12 @@ +In the code below, each line corresponds to the item in the task list. + +```js run +let admin, name; // can declare two variables at once + +name = "John"; + +admin = name; + +alert( admin ); // "John" +``` + diff --git a/1-js/02-first-steps/04-variables/1-hello-variables/task.md b/1-js/02-first-steps/04-variables/1-hello-variables/task.md new file mode 100644 index 00000000..84f009e8 --- /dev/null +++ b/1-js/02-first-steps/04-variables/1-hello-variables/task.md @@ -0,0 +1,10 @@ +importance: 2 + +--- + +# Working with variables + +1. Declare two variables: `admin` and `name`. +2. Assign the value `"John"` to `name`. +3. Copy the value from `name` to `admin`. +4. Show the value of `admin` using `alert` (must output "John"). diff --git a/1-js/02-first-steps/04-variables/2-declare-variables/solution.md b/1-js/02-first-steps/04-variables/2-declare-variables/solution.md new file mode 100644 index 00000000..69aa3558 --- /dev/null +++ b/1-js/02-first-steps/04-variables/2-declare-variables/solution.md @@ -0,0 +1,21 @@ +First, the variable for the name of our planet. + +That's simple: + +```js +let ourPlanetName = "Earth"; +``` + +Note, we could use a shorter name `planet`, but it might be not obvious what planet it refers to. It's nice to be more verbose. At least until the variable isNotTooLong. + +Second, the name of the current visitor: + +```js +let currentUserName = "John"; +``` + +Again, we could shorten that to `userName` if we know for sure that the user is current. + +Modern editors and autocomplete make long variable names easy to write. Don't save on them. A name with 3 words in it is fine. + +And if your editor does not have proper autocompletion, get [a new one](/editors). diff --git a/1-js/02-first-steps/04-variables/2-declare-variables/task.md b/1-js/02-first-steps/04-variables/2-declare-variables/task.md new file mode 100644 index 00000000..21631e26 --- /dev/null +++ b/1-js/02-first-steps/04-variables/2-declare-variables/task.md @@ -0,0 +1,8 @@ +importance: 3 + +--- + +# Giving the right name + +1. Create the variable with the name of our planet. How would you name such a variable? +2. Create the variable to store the name of the current visitor. How would you name that variable? diff --git a/1-js/02-first-steps/04-variables/3-uppercast-constant/solution.md b/1-js/02-first-steps/04-variables/3-uppercast-constant/solution.md new file mode 100644 index 00000000..f3a96c69 --- /dev/null +++ b/1-js/02-first-steps/04-variables/3-uppercast-constant/solution.md @@ -0,0 +1,5 @@ +We generally use upper case for constants that are "hard-coded". Or, in other words, when the value is known prior to execution and directly written into the code. + +In this code, `birthday` is exactly like that. So we could use the upper case for it. + +In contrast, `age` is evaluated in run-time. Today we have one age, a year after we'll have another one. It is constant in a sense that it does not change through the code execution. But it is a bit "less of a constant" than `birthday`, it is calculated, so we should keep the lower case for it. \ No newline at end of file diff --git a/1-js/02-first-steps/04-variables/3-uppercast-constant/task.md b/1-js/02-first-steps/04-variables/3-uppercast-constant/task.md new file mode 100644 index 00000000..5fd18f90 --- /dev/null +++ b/1-js/02-first-steps/04-variables/3-uppercast-constant/task.md @@ -0,0 +1,24 @@ +importance: 4 + +--- + +# Uppercase const? + +Examine the following code: + +```js +const birthday = '18.04.1982'; + +const age = someCode(birthday); +``` + +Here we have a constant `birthday` date and the `age` is calculated from `birthday` with the help of some code (it is not provided for shortness, and because details don't matter here). + +Would it be right to use upper case for `birthday`? For `age`? Or even for both? + +```js +const BIRTHDAY = '18.04.1982'; // make uppercase? + +const AGE = someCode(BIRTHDAY); // make uppercase? +``` + diff --git a/1-js/02-first-steps/04-variables/article.md b/1-js/02-first-steps/04-variables/article.md new file mode 100644 index 00000000..c9b4b270 --- /dev/null +++ b/1-js/02-first-steps/04-variables/article.md @@ -0,0 +1,332 @@ +# Variables + +Most of the time, a script needs to work with information. If it's an online-shop -- that's going to be the goods and a shopping cart. If it's a chat -- users, messages and so on. + +Variables are used to store the information. + +[cut] + +## A variable + +A [variable](https://en.wikipedia.org/wiki/Variable_(computer_science)) is a "named storage" for data. We can use variables to store goodies, visitors and other data. + +To create a variable in JavaScript, we need to use the `let` keyword. + +The statement below creates (in other words: *declares* or *defines*) a variable with the name "message": + +```js +let message; +``` + +Now we can put some data into it by using the assignment operator `=`: + +```js +let message; + +*!* +message = 'Hello'; // store the string +*/!* +``` + +The string is now saved into the memory area associated with the variable. We can access it using the variable name: + +```js run +let message; +message = 'Hello!'; + +*!* +alert(message); // shows the variable content +*/!* +``` + +To be concise we can merge the variable declaration and assignment into a single line: + +```js run +let message = 'Hello!'; // define the variable and assign the value + +alert(message); // Hello! +``` + +We can also declare multiple variables in one line: + +```js no-beautify +let user = 'John', age = 25, message = 'Hello'; +``` + +That might seem shorter, but it's not recommended. For the sake of better readability, please use a single line per variable. + +The multiline variant is a bit longer, but easier to read: + +```js +let user = 'John'; +let age = 25; +let message = 'Hello'; +``` + +Some people also write many variables like that: +```js no-beautify +let user = 'John', + age = 25, + message = 'Hello'; +``` + +...Or even in the "comma-first" style: + +```js no-beautify +let user = 'John' + , age = 25 + , message = 'Hello'; +``` + +Technically, all these variants do the same. 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`: + +```js +*!*var*/!* message = 'Hello'; +``` + +The `var` keyword is *almost* the same as `let`. It also declares a variable, but in a slightly different, "old-school" fashion. + +There are subtle differences between `let` and `var`, but they do not matter for us yet. We'll cover them in detail later, in the chapter . +```` + +## A real-life analogy + +We can easily grasp the concept of a "variable" if we imagine it as a "box" for data, with a unique-named sticker on it. + +For instance, the variable `message` can be imagined as a box labelled `"message"` with the value `"Hello!"` in it: + +![](variable.png) + +We can put any value into the box. + +Also we can change it. The value can be changed as many times as needed: + +```js run +let message; + +message = 'Hello!'; + +message = 'World!'; // value changed + +alert(message); +``` + +When the value is changed, the old data is removed from the variable: + +![](variable-change.png) + +We can also declare two variables and copy data from one into the other. + +```js run +let hello = 'Hello world!'; + +let message; + +*!* +// copy 'Hello world' from hello into message +message = hello; +*/!* + +// now two variables hold the same data +alert(hello); // Hello world! +alert(message); // Hello world! +``` + +```smart header="Functional languages" +It may be interesting to know that there also exist [functional](https://en.wikipedia.org/wiki/Functional_programming) programming languages that forbid changing a variable value. For example, [Scala](http://www.scala-lang.org/) or [Erlang](http://www.erlang.org/). + +In such languages, once the value is stored "in the box" -- it's there forever. If we need to store something else -- the language forces to create a new box (declare a new variable), we can't reuse the old one. + +Though it may seem a little bit odd at the first sight, these languages are quite capable of serious development. More than that, there are areas like parallel computations where this limitation infers certain benefits. Studying of such a language (even if not planning to use it soon) is recommended to broaden the mind. +``` + +## Variable naming [#variable-naming] + +There are two limitations for a variable name in JavaScript: + +1. The name must contain only letters, digits, symbols `$` and `_`. +2. The first character must not be a digit. + +Valid names, for instance: + +```js +let userName; +let test123; +``` + +When the name contains multiple words, [camelCase](https://en.wikipedia.org/wiki/CamelCase) is commonly used. That is: words go one after another, each word starts with a capital letter: `myVeryLongName`. + +What's interesting -- the dollar sign `'$'` and the underscore `'_'` also can be used in names. They are regular symbols, just like letters, without any special meaning. + +These names are valid: + +```js run untrusted +let $ = 1; // declared a variable with the name "$" +let _ = 2; // and now a variable with the name "_" + +alert($ + _); // 3 +``` + +Examples of incorrect variable names: + +```js no-beautify +let 1a; // cannot start with a digit + +let my-name; // a hyphen '-' is not allowed in the name +``` + +```smart header="Case matters" +Variables named `apple` and `AppLE` -- are two different variables. +``` + +````smart header="Non-english letters are allowed, but not recommended" +It is possible to use any language, including cyrillic letters or even hieroglyphs, like this: + +```js +let имя = '...'; +let 我 = '...'; +``` + +Technically, there is no error here, such names are allowed, but there is an international tradition to use English in variable names. Even if we're writing a small script, it may have a long life ahead. People from other countries may need to read it some time. +```` + +````warn header="Reserved names" +There is a list of reserved words, which cannot be used as variable names, because they are used by the language itself. + +For example, words `let`, `class`, `return`, `function` are reserved. + +The code below gives a syntax error: + +```js run no-beautify +let let = 5; // can't name a variable "let", error! +let return = 5; // also can't name it "return", error! +``` +```` + +````warn header="An assignment without `use strict`" + +Normally, we need to define a variable before using it. But in the old times, it was technically possible to create a variable by a mere assignment of the value, without `let`. This still works now if we don't put `use strict`, the behavior is kept for compatibility with old scripts. + +```js run no-strict +// note: no "use strict" in this example + +num = 5; // the variable "num" is created if didn't exist + +alert(num); // 5 +``` + +That's a bad practice, it gives an error in the strict mode: + +```js run untrusted +"use strict"; + +*!* +num = 5; // error: num is not defined +*/!* +``` + +```` + +## Constants + +To declare a constant (unchanging) variable, one can use `const` instead of `let`: + +```js +const myBirthday = '18.04.1982'; +``` + +The variable declared using `const` are called "constants". They can not be changed. An attempt to do it would cause an error: + +```js run +const myBirthday = '18.04.1982'; + +myBirthday = '01.01.2001'; // error, can't reassign the constant! +``` + +When a programmer is sure that the variable should never change, he can use `const` to guarantee it, and also to clearly show that fact to everyone. + + +### Uppercase constants + +There is a widespread practice to use constants as aliases for difficult-to-remember values that are known prior to execution. + +Such constants are named using capitals and underscores. + +Like this: + +```js run +const COLOR_RED = "#F00"; +const COLOR_GREEN = "#0F0"; +const COLOR_BLUE = "#00F"; +const COLOR_ORANGE = "#FF7F00"; + +// ...when we need to pick a color +let color = COLOR_ORANGE; +alert(color); // #FF7F00 +``` + +Benefits: + +- `COLOR_ORANGE` is much easier to remember than `"#FF7F00"`. +- It is much easier to mistype in `"#FF7F00"` than in `COLOR_ORANGE`. +- When reading the code -- `COLOR_ORANGE` is much more meaningful than `#FF7F00`. + +When should we use capitals for a constant, and when -- name them normally? Let's make that clear. + +Being a "constant" just means that the value never changes. But there are constants that are known prior to execution (like a hexadimal value for red), and there are those that are *calculated* in run-time, during the execution, but do not change after the assignment. + +For instance: +```js +const pageLoadTime = /* time taken by a webpage to load */; +``` + +The value of `pageLoadTime` is not known prior to the page load, so it's named normally. But it's still a constant, because it doesn't change after assignment. + +In other words, capital-named constants are only used as aliases for "hard-coded" values. + +## Name things right + +Talking about variables, there's one more exteremely important thing. + +Please name the variables sensibly. Take time to think if needed. + +Variable naming is one of the most important and complex skills in programming. A quick glance at variable names can reveal which code is written by a beginner and which by an experienced developer. + +In a real project, most of the time is spent on modifying and extending the existing code base, rather than writing something completely separate from the scratch. And when we return to the code after some time of doing something else, it's much easier to find the information that is well-labelled. Or, in other words, when the variables have good names. + +Please spend some time thinking about the right name for a variable before declaring it. That will repay you a lot. + +Some good-to-follow rules are: + +- Use human-readable names like `userName` or `shoppingCart`. +- Stay away from abbreviations or short names like `a`, `b`, `c`, unless you really know what you're doing. +- Make the name maximally descriptive and concise. Examples of bad names are `data` and `value`. Such a name says nothing. It is only ok to use them if it's exceptionally obvious from the context which data or value is meant. +- Agree on terms within the team and in your own mind. If a site visitor is called a "user" then we should name related variables like `currentUser` or `newUser`, but not `currentVisitor` or a `newManInTown`. + +Sounds simple? Indeed it is, but creating good descriptive-and-concise names in practice is not. Go for it. + +```smart header="Reuse or create?" +And the last note. There are some lazy programmers who, instead of declaring a new variable, tend to reuse the existing ones. + +As the result, the variable is like a box where people throw different things without changing the sticker. What is inside it now? Who knows... We need to come closer and check. + +Such a programmer saves a little bit on variable declaration, but looses ten times more on debugging the code. + +An extra variable is good, not evil. + +Modern JavaScript minifiers and browsers optimize code well enough, so it won't create performance issues. Using different variables for different values can even help the engine to optimize. +``` + +## Summary + +We can declare variables to store data. That can be done using `var` or `let` or `const`. + +- `let` -- is a modern variable declaration. The code must be in strict mode to use `let` in Chrome (V8). +- `var` -- is an old-school variable declaration. Normally we don't use it at all, but we'll cover subtle differences from `let` in the chapter , just in case if you'll need them. +- `const` -- is like `let`, but the value of variable can't be changed. + +Variables should be named in a way that allows to easily understand what's inside. diff --git a/1-js/02-first-steps/04-variables/variable-change.png b/1-js/02-first-steps/04-variables/variable-change.png new file mode 100644 index 00000000..6dd3803d Binary files /dev/null and b/1-js/02-first-steps/04-variables/variable-change.png differ diff --git a/1-js/02-first-steps/04-variables/variable-change@2x.png b/1-js/02-first-steps/04-variables/variable-change@2x.png new file mode 100644 index 00000000..f57b04ab Binary files /dev/null and b/1-js/02-first-steps/04-variables/variable-change@2x.png differ diff --git a/1-js/02-first-steps/04-variables/variable.png b/1-js/02-first-steps/04-variables/variable.png new file mode 100644 index 00000000..ab532d91 Binary files /dev/null and b/1-js/02-first-steps/04-variables/variable.png differ diff --git a/1-js/02-first-steps/04-variables/variable@2x.png b/1-js/02-first-steps/04-variables/variable@2x.png new file mode 100644 index 00000000..c9c37f03 Binary files /dev/null and b/1-js/02-first-steps/04-variables/variable@2x.png differ diff --git a/1-js/02-first-steps/05-types/1-string-quotes/solution.md b/1-js/02-first-steps/05-types/1-string-quotes/solution.md new file mode 100644 index 00000000..1e62735b --- /dev/null +++ b/1-js/02-first-steps/05-types/1-string-quotes/solution.md @@ -0,0 +1,15 @@ + +Backticks embed the expression inside `${...}` into the string. + +```js run +let name = "Ilya"; + +// the expression is a number 1 +alert( `hello ${1}` ); // Hello, 1 + +// the expression is a string "name" +alert( `hello ${"name"}` ); // Hello, name + +// the expression is a variable, embed it +alert( `hello ${name}` ); // Hello, Ilya +``` \ No newline at end of file diff --git a/1-js/02-first-steps/05-types/1-string-quotes/task.md b/1-js/02-first-steps/05-types/1-string-quotes/task.md new file mode 100644 index 00000000..14ea6b4d --- /dev/null +++ b/1-js/02-first-steps/05-types/1-string-quotes/task.md @@ -0,0 +1,17 @@ +importance: 5 + +--- + +# String quotes + +What is the output of the script? + +```js +let name = "Ilya"; + +alert( `hello ${1}` ); // ? + +alert( `hello ${"name"}` ); // ? + +alert( `hello ${name}` ); // ? +``` \ No newline at end of file diff --git a/1-js/02-first-steps/05-types/article.md b/1-js/02-first-steps/05-types/article.md new file mode 100644 index 00000000..2dd1ea0d --- /dev/null +++ b/1-js/02-first-steps/05-types/article.md @@ -0,0 +1,247 @@ +# Data types + +A variable in JavaScript can contain any data. A variable can at one moment be a string and later recieve a numeric value: + +```js +// no error +let message = "hello"; +message = 123456; +``` + +Programming languages that allow such thing are called "dynamically typed", meaning that there are data types, but variables are not bound to any of them. + +There are 7 basic data types in JavaScript. Here we'll study the basics, and in next chapters we'll talk about each of them in detail. + +[cut] + +## A number + +```js +let n = 123; +n = 12.345; +``` + +The *number* type serves both for integer and floating point numbers. + +There are many operations for numbers, e.g. multiplication `*`, division `/`, addition `+`, substraction `-` and so on. + +Besides regular numbers, there are so-called "special numeric values" which also belong to that type: `Infinity`, `-Infinity` and `NaN`. + +- `Infinity` represents the mathematical [Infinity](https://en.wikipedia.org/wiki/Infinity) ∞. It is a special value that's greater than any number. + + We can get it as a result of division by zero: + + ```js run + alert( 1 / 0 ); // Infinity + ``` + + Or just mention it in the code directly: + + ```js run + alert( Infinity ); // Infinity + ``` +- `NaN` represents a computational error. It is a result of an incorrect or an undefined mathematical operation, for instance: + + ```js run + alert( "not a number" / 2 ); // NaN, such division is erroneous + ``` + + `NaN` is sticky. Any further operation on `NaN` would give `NaN`: + + ```js run + alert( "not a number" / 2 + 5 ); // NaN + ``` + + So, if there's `NaN` somewhere in a mathematical expression, it propagates to the whole result. + +```smart header="Mathematical operations are safe" +Doing maths is safe in JavaScript. We can do anything: divide by zero, treat non-numeric strings as numbers, etc. + +The script will never stop with a fatal error ("die"). At worst we'll get `NaN` as the result. +``` + +Special numeric values formally belong to the "number" type. Of course they are not numbers in a common sense of this word. + +We'll see more into working with numbers in the chapter . + +## A string + +A string in JavaScript must be quoted. + +```js +let str = "Hello"; +let str2 = 'Single quotes are ok too'; +let phrase = `can embed ${str}`; +``` + +In JavaScript, there are 3 types of quotes. + +1. Double quotes: `"Hello"`. +2. Single quotes: `'Hello'`. +3. Backticks: `Hello`. + +Double and single quotes are "simple" quotes. There's no difference between them in JavaScript. + +Backticks are "extended functionality" quotes. They allow to embed variables and expressions into a string by wrapping them in `${…}`, for example: + +```js run +let name = "John"; + +// embed a variable +alert( `Hello, *!*${name}*/!*!` ); // Hello, John! + +// embed an expression +alert( `the result is *!*${1 + 2}*/!*` ); // the result is 3 +``` + +The expression inside `${…}` is evaluated and the result becomes a part of the string. We can put anything there: a variable like `name` or an arithmetical expression like `1 + 2` or something more complex. + +Please note that this only can be done in backticks, other quotes do not allow such embedding! +```js run +alert( "the result is ${1 + 2}" ); // the result is ${1 + 2} (double quotes do nothing) +``` + +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 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) + +The boolean type has only two values: `true` and `false`. + +This type is commonly used to store yes/no values: `true` means "yes, correct", and `false` means the "no, incorrect". + +For instance: + +```js +let nameFieldChecked = true; // yes, name field is checked +let ageFieldChecked = false; // no, age field is not checked +``` + +Boolean values also come as a result of comparisons: + +```js run +let isGreater = 4 > 1; + +alert( isGreater ); // true (the comparison result is "yes") +``` + +We'll cover booleans more deeply later in the chapter . + +## The "null" value + +The special `null` value does not belong to any type of those described above. + +It forms a separate type of its own, which contains only the `null` value: + +```js +let age = null; +``` + +In JavaScript `null` is not a "reference to a non-existing object" or a "null pointer" like in some other languages. + +It's just a special value which has the sense of "nothing", "empty" or "value unknown". + +The code above states that the `age` is unknown or empty for some reason. + +## The "undefined" value + +The special value `undefined` stands apart. It makes a type of its own, just like `null`. + +The meaning of `undefined` is "value is not assigned". + +If a variable is declared, but not assigned, then its value is exactly `undefined`: + +```js run +let x; + +alert(x); // shows "undefined" +``` + +Technically, it is possible to assign any variable to `undefined`: + +```js run +let x = 123; + +x = undefined; + +alert(x); // "undefined" +``` + +...But it's not recommended to do that. Normally, we use `null` to write an "empty" or an "unknown" value into the variable, and `undefined` is only used for checks, to see if the variable is assigned or similar. + +## 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 data and more complex entities. We'll deal with them later in the chapter after we know enough about primitives. + +The `symbol` type is used to create unique identifiers for objects. We have to mention it here for completeness, but it's better to study them after objects. + +## The typeof operator [#type-typeof] + +The `typeof` operator returns the type of the argument. It's useful when we want to process values of different types differently, or just want to make a quick check. + +It supports two forms of syntax: + +1. As an operator: `typeof x`. +2. Function style: `typeof(x)`. + +In other words, it works both with the brackets or without them. The result is the same. + +The call to `typeof x` returns a string with the type name: + +```js +typeof undefined // "undefined" + +typeof 0 // "number" + +typeof true // "boolean" + +typeof "foo" // "string" + +typeof Symbol("id") // "symbol" + +*!* +typeof Math // "object" (1) +*/!* + +*!* +typeof null // "object" (2) +*/!* + +*!* +typeof alert // "function" (3) +*/!* +``` + +The last three lines may need additional explanations: + +1. `Math` is a built-in object that provides mathematical operations. We will learn it in the chapter . Here it serves just as an example of an object. +2. The result of `typeof null` is `"object"`. That's wrong. It is an officially recognized error in `typeof`, kept for compatibility. Of course, `null` is not an object. It is a special value with a separate type of its own. So, again, that's an error in the language. +3. The result of `typeof alert` is `"function"`, because `alert` is a function of the language. We'll study functions in the next chapters, and we'll see that there's no special "function" type in the language. Functions belong to the object type. But `typeof` treats them differently. Formally, it's incorrect, but very convenient in practice. + + +## Summary + +There are 7 basic types in JavaScript. + +- `number` for numbers of any kind: integer or floating-point. +- `string` for strings. A string may have one more more characters, there's no separate single-character type. +- `boolean` for `true`/`false`. +- `null` for unknown values -- a standalone type that has a single value `null`. +- `undefined` for unassigned values -- a standalone type that has a single value `undefined`. +- `object` for more complex data structures. +- `symbol` for unique identifiers. + +The `typeof` operator allows to see which type is stored in the variable. + +- Two forms: `typeof x` or `typeof(x)`. +- Returns a string with the name of the type, like `"string"`. +- For `null` returns `"object"` -- that's the error in the language, it's not an object in fact. + +In the next chapters we'll concentrate on primitive values and once we're familiar with that, then we'll move on to objects. diff --git a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md b/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md new file mode 100644 index 00000000..0e1be244 --- /dev/null +++ b/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md @@ -0,0 +1,22 @@ + +```js no-beautify +"" + 1 + 0 = "10" // (1) +"" - 1 + 0 = -1 // (2) +true + false = 1 +6 / "3" = 2 +"2" * "3" = 6 +4 + 5 + "px" = "9px" +"$" + 4 + 5 = "$45" +"4" - 2 = 2 +"4px" - 2 = NaN +7 / 0 = Infinity +" -9\n" + 5 = " -9\n5" +" -9\n" - 5 = -14 +null + 1 = 1 // (3) +undefined + 1 = NaN // (4) +``` + +1. The addition with a string `"" + 1` converts `1` to a string: `"" + 1 = "1"`, and then we have `"1" + 0`, the same rule is applied. +2. The substruction `"-"` (like most math operations) only works with numbers, it converts an empty string `""` to `0`. +3. `null` becomes `0` after the numeric conversion. +4. `undefined` becomes `NaN` after the numeric conversion. diff --git a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md b/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md new file mode 100644 index 00000000..83b295f9 --- /dev/null +++ b/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md @@ -0,0 +1,26 @@ +importance: 5 + +--- + +# Type conversions + +What are results of these expressions? + +```js no-beautify +"" + 1 + 0 +"" - 1 + 0 +true + false +6 / "3" +"2" * "3" +4 + 5 + "px" +"$" + 4 + 5 +"4" - 2 +"4px" - 2 +7 / 0 +" -9\n" + 5 +" -9\n" - 5 +null + 1 +undefined + 1 +``` + +Think well, write down and then compare with the answer. diff --git a/1-js/02-first-steps/06-type-conversions/article.md b/1-js/02-first-steps/06-type-conversions/article.md new file mode 100644 index 00000000..25a4fd96 --- /dev/null +++ b/1-js/02-first-steps/06-type-conversions/article.md @@ -0,0 +1,162 @@ +# Type Conversions + +Most of time, operators and functions automatically convert a value to the right type. That's called "type coercion". + +For example, `alert` automatically converts any value to a string to show it. Mathematical operations convert values to numbers. + +There are also cases when we need to explicitly convert a value to put things right. + +[cut] + +```smart header="Not talking about objects yet" +In this chapter we don't cover objects yet. Here we study primitives first. Later, after we learn objects, we'll see how object conversion works in the chapter . +``` + +## ToString + +The string conversion happens when we need a string form of a value. + +For example, `alert(value)` does it to show the value. + +We can also use a call `String(value)` function for that: + +```js run +let value = true; +alert(typeof value); // boolean + +*!* +value = String(value); // now value is a string "true" +alert(typeof value); // string +*/!* +``` + +The string conversion is mostly obvious. A `false` becomes `"false"`, `null` becomes `"null"` etc. + +## ToNumber + +Numeric conversion happens in mathematical functions and expressions automatically. + +For example, when division `/` is applied to non-numbers: + +```js run +alert( "6" / "2" ); // 3, strings are converted to numbers +``` + +We can use a `Number(value)` function to explicitly convert a `value`: + +```js run +let str = "123"; +alert(typeof str); // string + +let num = Number(str); // becomes a number 123 + +alert(typeof num); // number +``` + +The explicit conversion is usually required when we read a value from a string-based source like a text form, but we expect a number to be entered. + +If the string is not a valid number, the result of such conversion is `NaN`, for instance: + +```js run +let age = Number("an arbitrary string instead of a number"); + +alert(age); // NaN, conversion failed +``` + +Numeric conversion rules: + +| Value | Becomes... | +|-------|-------------| +|`undefined`|`NaN`| +|`null`|`0`| +|true and false | `1` and `0` | +| `string` | Whitespaces from the start and the end are removed. Then, if the remaining string is empty, the result is `0`, otherwise -- the number is "read" from the string. An error gives `NaN`. | + +Examples: + +```js run +alert( Number(" 123 ") ); // 123 +alert( Number("123z") ); // NaN (error reading a number at "z") +alert( Number(true) ); // 1 +alert( Number(false) ); // 0 +``` + +Please note that `null` and `undefined` behave differently here: `null` becomes a zero, while `undefined` becomes `NaN`. + +````smart header="Addition '+' concatenates strings" +Almost all mathematical operations convert values to numbers. With a notable exception of the addition `+`. If one of the added values is a string, then another one is also converted to a string. + +Then it concatenates (joins) them: + +```js run +alert( 1 + '2' ); // '12' (string to the right) +alert( '1' + 2 ); // '12' (string to the left) +``` + +That only happens when one of arguments is a string. Otherwise values are converted to numbers. +```` + +## ToBoolean + +Boolean conversion is the simplest one. + +It happens in logical operations (later we'll meet condition tests and other kinds of them), but also can be performed manually with the call of `Boolean(value)`. + +The conversion rule: + +- Values that are intuitively "empty", like `0`, an empty string, `null`, `undefined` and `NaN` become `false`. +- Other values become `true`. + +For instance: + +```js run +alert( Boolean(1) ); // true +alert( Boolean(0) ); // false + +alert( Boolean("hello") ); // true +alert( Boolean("") ); // false +``` + +````warn header="Please note: the string with zero `\"0\"` is `true`" +Some languages (namely PHP) treat `"0"` as `false`. But in JavaScript a non-empty string is always `true`. + +```js run +alert( Boolean("0") ); // true +alert( Boolean(" ") ); // spaces, also true (any non-empty string is true) +``` +```` + + +## Summary + +There are three most widely used type conversions: to string, to number and to boolean. + +**`ToString`** -- occurs when we output something, can be performed with `String(value)`. The conversion to string is usually obvious for primitive values. + +**`ToNumber`** -- occurs in math operations, can be performed with `Number(value)`. + +The conversion follows the rules: + +| Value | Becomes... | +|-------|-------------| +|`undefined`|`NaN`| +|`null`|`0`| +|true / false | `1 / 0` | +| `string` | The string is read "as is", whitespaces from both sides are ignored. An empty string becomes `0`. An error gives `NaN`. | + +**`ToBoolean`** -- occurs in logical operations, or can be performed with `Boolean(value)`. + +Follows the rules: + +| Value | Becomes... | +|-------|-------------| +|`0`, `null`, `undefined`, `NaN`, `""` |`false`| +|any other value| `true` | + + +Most of these rules are easy to understand and memorize. The notable exceptions where people usually make mistakes are: + +- `undefined` is `NaN` as a number, not `0`. +- `"0"` and space-only strings like `" "` are true as a boolean. + +Objects are not covered here, we'll return to them later in the chapter that is devoted exclusively to objects, after we learn more basic things about JavaScript. diff --git a/1-js/02-first-steps/07-operators/1-increment-order/solution.md b/1-js/02-first-steps/07-operators/1-increment-order/solution.md new file mode 100644 index 00000000..8a44d798 --- /dev/null +++ b/1-js/02-first-steps/07-operators/1-increment-order/solution.md @@ -0,0 +1,18 @@ + +The answer is: + +- `a = 2` +- `b = 2` +- `c = 2` +- `d = 1` + +```js run no-beautify +let a = 1, b = 1; + +alert( ++a ); // 2, prefix form returns the new value +alert( b++ ); // 1, postfix form returns the old value + +alert( a ); // 2, incremented once +alert( b ); // 2, incremented once +``` + diff --git a/1-js/02-first-steps/07-operators/1-increment-order/task.md b/1-js/02-first-steps/07-operators/1-increment-order/task.md new file mode 100644 index 00000000..7db09238 --- /dev/null +++ b/1-js/02-first-steps/07-operators/1-increment-order/task.md @@ -0,0 +1,14 @@ +importance: 5 + +--- + +# The postfix and prefix forms + +What are the final values of all variables `a`, `b`, `c` and `d` after the code below? + +```js +let a = 1, b = 1; + +let c = ++a; // ? +let d = b++; // ? +``` diff --git a/1-js/02-first-steps/07-operators/2-assignment-result/solution.md b/1-js/02-first-steps/07-operators/2-assignment-result/solution.md new file mode 100644 index 00000000..e3113b4c --- /dev/null +++ b/1-js/02-first-steps/07-operators/2-assignment-result/solution.md @@ -0,0 +1,5 @@ +The answer is: + +- `a = 4` (multiplied by 2) +- `x = 5` (calculated as 1 + 4) + diff --git a/1-js/02-first-steps/07-operators/2-assignment-result/task.md b/1-js/02-first-steps/07-operators/2-assignment-result/task.md new file mode 100644 index 00000000..5345c948 --- /dev/null +++ b/1-js/02-first-steps/07-operators/2-assignment-result/task.md @@ -0,0 +1,13 @@ +importance: 3 + +--- + +# Assignment result + +What are the values of `a` and `x` after the code below? + +```js +let a = 2; + +let x = 1 + (a *= 2); +``` diff --git a/1-js/02-first-steps/07-operators/article.md b/1-js/02-first-steps/07-operators/article.md new file mode 100644 index 00000000..e679df61 --- /dev/null +++ b/1-js/02-first-steps/07-operators/article.md @@ -0,0 +1,439 @@ +# Operators + +Many operators are known to us from school. It is an addition `+`, a multiplication `*`, a substraction `-` and so on. + +In this chapter we concentrate on aspects that are not covered by the school arithmetic. + +[cut] + +## Terms: "unary", "binary", "operand" + +Before we move on, let's grasp the common terminology. + +- *An operand* -- is what operators are applied to. For instance in multiplication `5 * 2` there are two operands: the left operand is `5`, and the right operand is `2`. Sometimes people say "arguments" instead of "operands". +- An operator is *unary* if it has a single operand. For example, the unary minus `"-"` reverses the sign of the number: + + ```js run + let x = 1; + + *!* + x = -x; + */!* + alert( x ); // -1, unary minus was applied + ``` +- An operator is *binary* if it has two operands. The same minus exists in the binary form as well: + + ```js run no-beautify + let x = 1, y = 3; + alert( y - x ); // 2, binary minus substracts values + ``` + + Formally, we're talking about the two different operators here: the unary minus (single operand, reverses the sign) and the binary minus (two operands, substracts). + +## Strings concatenation, binary + + +Now let's see special features of JavaScript operators that are beyond school arithmetics. + +Usually the plus operator `'+'` sums numbers. + +But if the binary `+` is applied to strings, it merges (concatenates) them: + +```js +let s = "my" + "string"; +alert(s); // mystring +``` + +Note that if any of operands is a string, then the other one is converted to string too. + +For example: + +```js run +alert( '1' + 2 ); // "12" +alert( 2 + '1' ); // "21" +``` + +See, it doesn't matter whether the first operand is a string or the second one. The rule is simple: if any of operands is a string, then convert the other one into a string as well. + +The string concatenation and conversion is the special feature of the binary plus `"+"`. Other arithmetic operators work only with numbers. They always convert their operands to numbers. + +For instance, subtraction and division: + +```js run +alert( 2 - '1' ); // 1 +alert( '6' / '2' ); // 3 +``` + +## Numeric conversion, unary + + +The plus `+` exist in two forms. The binary form that we used above and the unary form. + +The unary plus or, in other words, the plus operator `+` applied to a single value, doesn't do anything with numbers, but if the operand is not a number, then it is converted into it. + +For example: + +```js run +// No effect on numbers +let x = 1; +alert( +x ); // 1 + +let y = -2; +alert( +y ); // -2 + +*!* +// Converts non-numbers +alert( +true ); // 1 +alert( +"" ); // 0 +*/!* +``` + +It actually does the same as `Number(...)`, but shorter. + +A need to convert string to number arises very often. For example, if we are getting values from HTML form fields, then are usually strings. + +What if we want to sum them? + +The binary plus would add them as strings: + +```js run +let apples = "2"; +let oranges = "3"; + +alert( apples + oranges ); // "23", the binary plus concatenates strings +``` + +If we want to treat them as numbers, then we can convert and then sum: + +```js run +let apples = "2"; +let oranges = "3"; + +*!* +// both values converted to numbers before the binary plus +alert( +apples + +oranges ); // 5 +*/!* + +// the longer variant +// alert( Number(apples) + Number(oranges) ); // 5 +``` + +From a mathematician's standpoint the abundance of pluses may seem strange. But from a programmer's standpoint -- there's nothing special: unary pluses are applied first, they convert strings to numbers, and then the binary plus sums them up. + +Why are unary pluses applied to values before the binary one? As we're going to see, that's because of their *higher precedence*. + +## Operators precedence + +If an expression has more than one operator, the execution order is defined by their *precedence*, or, in other words, there's an implicit priority order among the operators. + +From the school we all know that the multiplication in the expression `1 + 2 * 2` should be calculated before the addition. That's exactly the precedence thing. The multiplication is said to have *a higher precedence* than the addition. + +Parentheses override any precedence, so if we're not satisfied with the order, we can use them, like: `(1 + 2) * 2`. + +There are many operators in JavaScript. Every operator has a corresponding precedence number. The one with the bigger number executes first. If the precedence is same -- the execution order is from left to right. + +An extract from the [precedence table](https://developer.mozilla.org/en/JavaScript/Reference/operators/operator_precedence): + +| Precedence | Name | Sign | +|------------|------|------| +| ... | ... | ... | +| 15 | unary plus | `+` | +| 15 | unary minus | `-` | +| 14 | multiplication | `*` | +| 14 | division | `/` | +| 13 | addition (binary) | `+` | +| 13 | subtraction | `-` | +| ... | ... | ... | +| 3 | assignment | `=` | +| ... | ... | ... | + +As we can see, the "unary plus" has a priority of `15`, higher than `13` for the "addition" (binary plus). That's why in the expression `"+apples + +oranges"` unary pluses work first, and then the addition. + +## Assignment + +Let's note that an assignment `=` is also an operator. It is listed in the precedence table with the very low priority of `3`. + +That's why when we assign a variable, like `x = 2 * 2 + 1`, then the calculations are done first, and afterwards the `=` is evaluated, storing the result in `x`. + +```js +let x = 2 * 2 + 1; + +alert( x ); // 5 +``` + +It is possible to chain assignments: + +```js run +let a, b, c; + +*!* +a = b = c = 2 + 2; +*/!* + +alert( a ); // 4 +alert( b ); // 4 +alert( c ); // 4 +``` + +Chained assignments evaluate from right to left. First the rightmost expression `2+2` is evaluated then assigned to the variables on the left: `c`, `b` and `a`. At the end, all variables share a single value. + +````smart header="The assignment operator `\"=\"` returns a value" +An operator always returns a value. That's obvious for most of them like an addition `+` or a multiplication `*`. But the assignment operator follows that rule too. + +The call `x = value` writes the `value` into `x` *and then returns it*. + +Here's the demo that uses an assignment as the part of a more complex expression: + +```js run +let a = 1; +let b = 2; + +*!* +let c = 3 - (a = b + 1); +*/!* + +alert( a ); // 3 +alert( c ); // 0 +``` + +In the example above, the result of `(a = b + 1)` is the value which is assigned to `a` (that is `3`). It is then used to substract from `3`. + +Funny code, isn't it? We should understand how it works, because sometimes we can see it in 3rd-party libraries, but shouldn't write anything like that ourselves. Such tricks definitely don't make the code clearer and readable. +```` + +## Remainder % + +The remainder operator `%` despite it's look does not have a relation to percents. + +The result of `a % b` is the remainder of the integer division of `a` by `b`. + +For instance: + +```js run +alert( 5 % 2 ); // 1 is a remainder of 5 divided by 2 +alert( 8 % 3 ); // 2 is a remainder of 8 divided by 3 +alert( 6 % 3 ); // 0 is a remainder of 6 divided by 3 +``` + +## Exponentiation ** + +The exponentiation operator `**` is a recent addition to the language. + +For a natural number `b`, the result of `a ** b` is `a` multiplied by itself `b` times. + +For instance: + +```js run +alert( 2 ** 2 ); // 4 (2 * 2) +alert( 2 ** 3 ); // 8 (2 * 2 * 2) +alert( 2 ** 4 ); // 16 (2 * 2 * 2 * 2) +``` + +The operator works for non-integer numbers of `a` and `b` as well, for instance: + +```js run +alert( 4 ** (1/2) ); // 2 (power of 1/2 is the same as a square root, that's maths) +alert( 8 ** (1/3) ); // 2 (power of 1/3 is the same as a cubic root) +``` + +## Increment/decrement + + + +Increasing or decreasing a number by one is among the most common numerical operations. + +So, there are special operators for that: + +- **Increment** `++` increases a variable by 1: + + ```js run no-beautify + let counter = 2; + counter++; // works same as counter = counter + 1, but shorter + alert( counter ); // 3 + ``` +- **Decrement** `--` decreases a variable by 1: + + ```js run no-beautify + let counter = 2; + counter--; // works same as counter = counter - 1, but shorter + alert( counter ); // 1 + ``` + +```warn +Increment/decrement can be applied only to a variable. An attempt to use it on a value like `5++` will give an error. +``` + +Operators `++` and `--` can be placed both after and before the variable. + +- When the operator goes after the variable, it is called a "postfix form": `counter++`. +- The "prefix form" is when the operator stands before the variable: `++counter`. + +Both of these records do the same: increase `i` by `1`. + +Is there any difference? Yes, but we can only see it if we use the retured value of `++/--`. + +Let's clarify. As we know, all operators return a value. Increment/decrement is not an exception here. The prefix form returns the new value, while the postfix form returns the old value (prior to increment/decrement). + +To see the difference -- here's the example: + +```js run +let counter = 1; +let a = ++counter; // (*) + +alert(a); // *!*2*/!* +``` + +Here in the line `(*)` the prefix call `++counter` increments `i` and returns the new value that is `2`. So the `alert` shows `2`. + +Now let's use the postfix form: + +```js run +let counter = 1; +let a = counter++; // (*) changed ++counter to counter++ + +alert(a); // *!*1*/!* +``` + +In the line `(*)` the *postfix* form `counter++` also increments `i`, but returns the *old* value (prior to increment). So the `alert` shows `1`. + +To summarize: + +- If the result of increment/decrement is not used, then there is no difference which form to use: + + ```js run + let counter = 0; + counter++; + ++counter; + alert( counter ); // 2, the lines above did the same + ``` +- If we'd like to increase the value *and* use the result of the operator right now, then we need the prefix form: + + ```js run + let counter = 0; + alert( ++counter ); // 1 + ``` +- If we'd like to increment, but use the previous value, then we need the postfix form: + + ```js run + let counter = 0; + alert( counter++ ); // 0 + ``` + +````smart header="Increment/decrement among other operators" +Operators `++/--` can be used inside an expression as well. Their precedence is higher than most other arithmetical operations. + +For instance: + +```js run +let counter = 1; +alert( 2 * ++counter ); // 4 +``` + +Compare with: + +```js run +let counter = 1; +alert( 2 * counter++ ); // 2, because counter++ returns the "old" value +``` + +Though technically allowable, such notation usually makes the code less readable. One line does multiple things -- not good. + +While reading the code, a fast "vertical" eye-scan can easily miss such `counter++`, and it won't be obvious that the variable increases. + +The "one line -- one action" style is advised: + +```js run +let counter = 1; +alert( 2 * counter ); +counter++; +``` +```` + +## Bitwise operators + +Bitwise operators treat arguments as 32-bit integer numbers and work on the level of their binary representation. + +These operators are not JavaScript-specific. They are supported in most programming languages. + +The list of operators: + +- AND ( `&` ) +- OR ( `|` ) +- XOR ( `^` ) +- NOT ( `~` ) +- LEFT SHIFT ( `<<` ) +- RIGHT SHIFT ( `>>` ) +- ZERO-FILL RIGHT SHIFT ( `>>>` ) + +These operators are used very rarely. To understand them, we should delve into low-level number representation, and it would not be optimal to do that right now. Especially because we won't need them any time soon. If you're curious, you can read the [Bitwise Operators](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators) article in MDN. It would be more practical to do that when a real need arises. + +## Modify-in-place + +We often need to apply an operator to a variable and store the new result in it. + +For example: + +```js +let n = 2; +n = n + 5; +n = n * 2; +``` + +This notation can be shortened using operators `+=` and `*=`: + +```js run +let n = 2; +n += 5; // now n=7 (same as n = n + 5) +n *= 2; // now n=14 (same as n = n * 2) + +alert( n ); // 14 +``` + +Short "modify-and-assign" operators exist for all arithmetical and bitwise operators: `/=`, `-=` etc. + +Such operators have the same precedence as a normal assignment, so they run after most other calculations: + +```js run +let n = 2; + +n *= 3 + 5; + +alert( n ); // 16 (right part evaluated first, same as n *= 8) +``` + +## Comma + +The comma operator `','` is one of most rare and unusual operators. Sometimes it's used to write shorter code, so we need to know it in order to understand what's going on. + +The comma operator allows to evaluate several expressions, dividing them with a comma `','`. Each of them is evaluated, but result of only the last one is returned. + +For example: + +```js run +*!* +a = (1+2, 3+4); +*/!* + +alert( a ); // 7 (the result of 3+4) +``` + +Here, the first expression `1+2` is evaluated, and it's result is thrown away, then `3+4` is evaluated and returned as the result. + +```smart header="Comma has a very low precedence" +Please note that the comma operator has very low precedence, lower than `=`, so parentheses are important in the example above. + +Without them: `a=1+2,3+4` evaluates `+` first, summing the numbers into `a=3,7`, then the assignment operator `=` assigns `a=3`, and then the number after the comma `7` is not processed anyhow, so it's ignored. +``` + +Why do we need such an operator which throws away everything except the last part? + +Sometimes people use it in more complex constructs to put several actions in one line. + +For example: + +```js +// three operations in one line +for (*!*a = 1, b = 3, c = a * b*/!*; a < 10; a++) { + ... +} +``` + +Such tricks are used in many JavaScript frameworks, that's why we mention about them. But usually they don't improve the code readability, so we should think well before writing like that. diff --git a/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md b/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md new file mode 100644 index 00000000..e1f50ce1 --- /dev/null +++ b/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md @@ -0,0 +1,21 @@ + + +```js no-beautify +5 > 4 → true +"apple" > "pineapple" → false +"2" > "12" → true +undefined == null → true +undefined === null → false +null == "\n0\n" → false +null === +"\n0\n" → false +``` + +Some of the reasons: + +1. Obviously, true. +2. Dictionary comparison, hence true. +3. Again, dictionary comparison, first char of `"2"` is greater than the first char of `"1"`. +4. Values `null` and `undefined` equal each other only. +5. Strict equality is strict. Different types from both sides lead to false. +6. See (4). +7. Strict equality of different types. diff --git a/1-js/02-first-steps/08-comparison/1-comparison-questions/task.md b/1-js/02-first-steps/08-comparison/1-comparison-questions/task.md new file mode 100644 index 00000000..891e703d --- /dev/null +++ b/1-js/02-first-steps/08-comparison/1-comparison-questions/task.md @@ -0,0 +1,18 @@ +importance: 5 + +--- + +# Comparisons + +What will be the result for expressions? + +```js no-beautify +5 > 4 +"apple" > "pineapple" +"2" > "12" +undefined == null +undefined === null +null == "\n0\n" +null === +"\n0\n" +``` + diff --git a/1-js/02-first-steps/08-comparison/article.md b/1-js/02-first-steps/08-comparison/article.md new file mode 100644 index 00000000..fca656c2 --- /dev/null +++ b/1-js/02-first-steps/08-comparison/article.md @@ -0,0 +1,214 @@ +# Comparisons + +Many comparison operators we know from maths: + +- Greater/less than: a > b, a < b. +- Greater/less than or equals: a >= b, a <= b. +- Equality check is written as `a == b` (please note the double equation sign `'='`. A single symbol `a = b` would mean an assignment). +- Not equals. In maths the notation is , in JavaScript it's written as an assignment with an exclamation sign before it: a != b. + +[cut] + +## Boolean is the result + +Just as all other operators, a comparison returns a value. The value is of the boolean type. + +- `true` -- means "yes", "correct" or "the truth". +- `false` -- means "no", "wrong" or "a lie". + +For example: + +```js run +alert( 2 > 1 ); // true (correct) +alert( 2 == 1 ); // false (wrong) +alert( 2 != 1 ); // true (correct) +``` + +A comparison result can be assigned to a variable, just like any value: + +```js run +let result = 5 > 4; // assign the result of the comparison +alert( result ); // true +``` + +## String comparison + +To see which string is greater than the other, the so-called "dictionary" or "lexicographical" order is used. + +In other words, strings are compared letter-by-letter. + +For example: + +```js run +alert( 'Z' > 'A' ); // true +alert( 'Glow' > 'Glee' ); // true +alert( 'Bee' > 'Be' ); // true +``` + +The algorithm to compare two strings is simple: + +1. Compare first characters of both strings. +2. If the first one is greater(or less), then the first string is greater(or less) than the second. We're done. +3. Otherwise if first characters are equal, compare the second characters the same way. +4. Repeat until the end of any string. +5. If both strings ended simultaneously, then they are equal. Otherwise the longer string is greater. + +In the example above, the comparison `'Z' > 'A'` gets the result at the first step. + +Strings `"Glow"` and `"Glee"` are compared character-by-character: + +1. `G` is the same as `G`. +2. `l` is the same as `l`. +3. `o` is greater than `e`. Stop here. The first string is greater. + +```smart header="Not a real dictionary, but Unicode order" +The comparison algorithm given above is roughly equivalent to the one used in book dictionaries or phone books. But it's not exactly the same. + +For instance, case matters. A capital letter `"A"` is not equal to the lowercase `"a"`. Which one is greater? Actually, the lowercase `"a"` is. Why? Because the lowercase character has a greater index in the internal encoding table (Unicode). We'll get back to specific details and consequences in the chapter . +``` + +## Comparison of different types + +When compared values belong to different types, they are converted to numbers. + +For example: + +```js run +alert( '2' > 1 ); // true, string '2' becomes a number 2 +alert( '01' == 1 ); // true, string '01' becomes a number 1 +``` + +For boolean values, `true` becomes `1` and `false` becomes `0`, that's why: + +```js run +alert( true == 1 ); // true +alert( false == 0 ); // true +``` + +````smart header="A funny consequence" +It is possible that in the same time: + +- Two values are equal. +- One of them is `true` as a boolean and the other one is `false` as a boolean. + +For example: + +```js run +let a = 0; +alert( Boolean(a) ); // false + +let b = "0"; +alert( Boolean(b) ); // true + +alert(a == b); // true! +``` + +From JavaScript standpoint that's quite normal. An equality check converts using the numeric conversion (hence `"0"` becomes `0`), while `Boolean` conversion uses another set of rules. +```` + +## Strict equality + +A regular equality check `"=="` has a problem. It cannot differ `0` from `false`: + +```js run +alert( 0 == false ); // true +``` + +The same thing with an empty string: + +```js run +alert( '' == false ); // true +``` + +That's because operands of different types are converted to a number by the assignment operator `=`. An empty string, just like `false`, becomes a zero. + +What to do if we'd like to differentiate `0` from `false`? + +**A strict equality operator `===` checks the equality without type conversion.** + +In other words, if `a` and `b` are of different types then `a === b` immediately returns `false`, without an attempt to convert them. + +Let's try it: + +```js run +alert( 0 === false ); // false, because the types are different +``` + +There also exists a "strict non-equality" operator `!==`, as an analogy for `!=`. + +The string equality check operator is a bit longer to write, but makes it obvious what's going on and leaves less space for errors. + +## Comparison with null and undefined + +Let's see more edge cases. + +There's a non-intuitive behavior when `null` or `undefined` is compared with other values. + + +For a strict equality check `===` +: These values are different, because each of them belong to a separate type of it's own. + + ```js run + alert( null === undefined ); // false + ``` + +For a non-strict check `==` +: There's a special rule. These two are a "sweet couple": they equal each other (in the sense of `==`), but no any other value. + + ```js run + alert( null == undefined ); // true + ``` + +For maths and other comparisons `< > <= >=` +: Values `null/undefined` are converted to a number: `null` becomes `0`, while `undefined` becomes `NaN`. + +Now let's see funny things that happen when we apply those rules. And, what's more important, how do not fall into a trap with these features. + +### Strange result: null vs 0 + +Let's compare `null` with a zero: + +```js run +alert( null > 0 ); // (1) false +alert( null == 0 ); // (2) false +alert( null >= 0 ); // (3) *!*true*/!* +``` + +Yeah, mathematically that's strange. The last result states that "`null` is equal or greater than zero". Then one of the comparisons above must be correct, but they are both falsy. + +The reason is that an equality check `==` and comparisons `> < >= <=` work differently. Comparisons convert `null` to a number, hence treat it as `0`. That's why (1) `null >= 0` is true and (3) `null > 0` is false. + +From the other hand, the equality check `==` for `undefined` and `null` works by the rule, without any conversions. They equal each other and don't equal anything else. That's why (2) `null == 0` is false. + +### An incomparable undefined + +The value `undefined` shouldn't participate in comparisons at all: + +```js run +alert( undefined > 0 ); // false (1) +alert( undefined < 0 ); // false (2) +alert( undefined == 0 ); // false (3) +``` + +Why does it dislike a zero so much? Always false! + +We've got such result, because: + +- Comparisons `(1)` and `(2)` return `false` because `undefined` gets converted to `NaN`. And `NaN` is a special numeric value which returns `false` for all comparisons. +- The equality check `(3)` returns `false`, because `undefined` only equals `null` and no other value. + +### Evade problems + +Why did we observe these examples? Should we remember these pecularities all the time? Well, not really. Actually, these tricky things will gradually become familiar over the time, but there's a solid way to evade any problems with them. + +Just treat any comparison with `undefined/null` except the strict equality `===` with an exceptional care. + +Don't use comparisons `>= > < <=` with a variable which may be `null/undefined`, unless you are really sure what you're doing. If a variable can have such values, then check for them separately. + +## Summary + +- Comparison operators return a logical value. +- Strings are compared letter-by-letter in the "dictionary" order. +- When values of different types are compared, they get converted to numbers (with the exclusion of a strict equality check). +- Values `null` and `undefined` equal `==` each other and do not equal any other value. +- Be careful when using comparisons like `>` or `<` with variables that can occasionaly be `null/undefined`. Making a separate check for `null/undefined` is a good idea. diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/solution.md b/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/solution.md new file mode 100644 index 00000000..903ee7ff --- /dev/null +++ b/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/solution.md @@ -0,0 +1,24 @@ +JavaScript-code: + +```js demo run +let name = prompt("What is your name?", ""); +alert(name); +``` + +The full page: + +```html + + + + + + + + +``` diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/task.md b/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/task.md new file mode 100644 index 00000000..a65a654e --- /dev/null +++ b/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/task.md @@ -0,0 +1,9 @@ +importance: 4 + +--- + +# A simple page + +Create a web-page that asks for a name and outputs it. + +[demo] diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/article.md b/1-js/02-first-steps/09-alert-prompt-confirm/article.md new file mode 100644 index 00000000..d72bf5bf --- /dev/null +++ b/1-js/02-first-steps/09-alert-prompt-confirm/article.md @@ -0,0 +1,113 @@ +# Interaction: alert, prompt, confirm + +This part of the tutorial aims to cover JavaScript "as is", without environment-specific tweaks. + +But still we use a browser as the demo environment. So we should know at least few user-interface functions. + +In this chapter we'll get familiar with the browser-specific functions `alert`, `prompt` and `confirm`. + +[cut] + +## alert + +Syntax: + +```js +alert(message); +``` + +This shows a message and pauses the script execution until the user presses "OK". + +For example: + +```js run +alert("Hello"); +``` + +The mini-window with the message is called a *modal window*. The word "modal" means that the visitor can't interact with the rest of the page, press other buttons etc, until he deals with the window. In this case -- until he presses "OK". + +## prompt + +Function `prompt` accepts two arguments: + +```js no-beautify +result = prompt(title[, default]); +``` + +It shows a modal window with a text message, an input field for the visitor and buttons OK/CANCEL. + +`title` +: The text to show to the visitor. + +`default` +: An optional second parameter, the initial value for the input field. + +The visitor may type something in the prompt input field and press OK. Or he can cancel the input by pressing a CANCEL button or hitting the `key:Esc` key. + +The call to `prompt` returns the text from the field or `null` if the input was canceled. + +For instance: + +```js run +let age = prompt('How old are you?', 100); + +alert(`You are ${age} years old!`); // You are 100 years old! +``` + +````warn header="IE: always supply a `default`" +The second parameter is optional. But if we don't supply it, Internet Explorer would insert the text `"undefined"` into the prompt. + +Run this code in Internet Explorer to see that: + +```js run +let test = prompt("Test"); +``` + +So, to look good in IE, it's recommended to always provide the second argument: + +```js run +let test = prompt("Test", ''); // <-- for IE +``` +```` + +## confirm + +The syntax: + +```js +result = confirm(question); +``` + +Function `confirm` shows a modal window with a `question` and two buttons: OK and CANCEL. + +The result is `true` if OK is pressed and `false` otherwise. + +For example: + +```js run +let isBoss = confirm("Are you the boss?"); + +alert( isBoss ); // true is OK is pressed +``` + +## Summary + +We covered 3 browser-specific functions to interact with the visitor: + +`alert` +: shows a message. + +`prompt` +: shows a message asking the user to input text. It returns the text or, if CANCEL or `key:Esc` is clicked, all browsers except Safari return `null`. + +`confirm` +: shows a message and waits the user to press "OK" or "CANCEL". It returns `true` for OK and `false` for CANCEL/`key:Esc`. + +All these methods are modal: they pause the script execution and don't let the visitor to interact with the rest of the page until he dismisses them. + +There are two limitations shared by all the methods above: + +1. The exact location of the modal window is determined by the browser. Usually it's in the center. +2. The exact look of the window also depends on the browser. We can't modify it. + +That is the price for simplicity. There are other ways to show nicer windows and richer interaction with the visitor, but if "bells and whistles" do not matter much, these methods work just fine. diff --git a/1-js/02-first-steps/10-ifelse/1-if-zero-string/solution.md b/1-js/02-first-steps/10-ifelse/1-if-zero-string/solution.md new file mode 100644 index 00000000..51f1d468 --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/1-if-zero-string/solution.md @@ -0,0 +1,12 @@ +**Yes, it will.** + +Any string except an empty one (and `"0"` is not empty) becomes `true` in the logical context. + +We can run and check: + +```js run +if ("0") { + alert( 'Hello' ); +} +``` + diff --git a/1-js/02-first-steps/10-ifelse/1-if-zero-string/task.md b/1-js/02-first-steps/10-ifelse/1-if-zero-string/task.md new file mode 100644 index 00000000..5f16cda8 --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/1-if-zero-string/task.md @@ -0,0 +1,14 @@ +importance: 5 + +--- + +# if (a string with zero) + +Will `alert` be shown? + +```js +if ("0") { + alert( 'Hello' ); +} +``` + diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.png b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.png new file mode 100644 index 00000000..8c57b188 Binary files /dev/null and b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.png differ diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2/index.html b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2/index.html new file mode 100644 index 00000000..ea996665 --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2@2x.png b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2@2x.png new file mode 100644 index 00000000..cbb2c611 Binary files /dev/null and b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2@2x.png differ diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/solution.md b/1-js/02-first-steps/10-ifelse/2-check-standard/solution.md new file mode 100644 index 00000000..99dea945 --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/2-check-standard/solution.md @@ -0,0 +1,4 @@ + + +[html run src="ifelse_task2/index.html"] + diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/task.md b/1-js/02-first-steps/10-ifelse/2-check-standard/task.md new file mode 100644 index 00000000..46fe05dc --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/2-check-standard/task.md @@ -0,0 +1,14 @@ +importance: 2 + +--- + +# The name of JavaScript + +Using the `if..else` construct, write the code which asks: 'What is the "official" name of JavaScript?' + +If the visitor enters "ECMAScript", then output "Right!", otherwise -- output: "Didn't know? ECMAScript!" + +![](ifelse_task2.png) + +[demo src="ifelse_task2"] + diff --git a/1-js/02-first-steps/10-ifelse/3-sign/if_sign/index.html b/1-js/02-first-steps/10-ifelse/3-sign/if_sign/index.html new file mode 100644 index 00000000..f168360d --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/3-sign/if_sign/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/1-js/02-first-steps/10-ifelse/3-sign/solution.md b/1-js/02-first-steps/10-ifelse/3-sign/solution.md new file mode 100644 index 00000000..262a605c --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/3-sign/solution.md @@ -0,0 +1,14 @@ + + +```js run +let value = prompt('Type a number', 0); + +if (value > 0) { + alert( 1 ); +} else if (value < 0) { + alert( -1 ); +} else { + alert( 0 ); +} +``` + diff --git a/1-js/02-first-steps/10-ifelse/3-sign/task.md b/1-js/02-first-steps/10-ifelse/3-sign/task.md new file mode 100644 index 00000000..0c5d0e00 --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/3-sign/task.md @@ -0,0 +1,15 @@ +importance: 2 + +--- + +# Show the sign + +Using `if..else`, write the code which gets a number via `prompt` and then shows in `alert`: + +- `1`, if the value is greater than zero, +- `-1`, if less than zero, +- `0`, if equals zero. + +In this task we assume that the input is always a number. + +[demo src="if_sign"] diff --git a/1-js/02-first-steps/10-ifelse/4-check-login/ifelse_task.png b/1-js/02-first-steps/10-ifelse/4-check-login/ifelse_task.png new file mode 100644 index 00000000..8b54dc83 Binary files /dev/null and b/1-js/02-first-steps/10-ifelse/4-check-login/ifelse_task.png differ diff --git a/1-js/02-first-steps/10-ifelse/4-check-login/ifelse_task@2x.png b/1-js/02-first-steps/10-ifelse/4-check-login/ifelse_task@2x.png new file mode 100644 index 00000000..92001dfe Binary files /dev/null and b/1-js/02-first-steps/10-ifelse/4-check-login/ifelse_task@2x.png differ diff --git a/1-js/02-first-steps/10-ifelse/4-check-login/solution.md b/1-js/02-first-steps/10-ifelse/4-check-login/solution.md new file mode 100644 index 00000000..20abeb43 --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/4-check-login/solution.md @@ -0,0 +1,31 @@ + + +```js run demo +let userName = prompt("Who's there?", ''); + +if (userName == 'Admin') { + + let pass = prompt('Password?', ''); + + if (pass == 'TheMaster') { + alert( 'Welcome!' ); + } else if (pass == null || pass == '') { // (*) + alert( 'Canceled.' ); + } else { + alert( 'Wrong password' ); + } + +} else if (userName == null || userName == '') { // (**) + + alert( 'Canceled' ); + +} else { + + alert( "I don't know you" ); + +} +``` + +Please note the `if` check in lines `(*)` and `(**)`. Every browser except Safari returns `null` when the input is canceled, and Safari returns an empty string. So we must treat them same for compatibility. + +Also note the vertical indents inside the `if` blocks. They are technically not required, but make the code more readable. diff --git a/1-js/02-first-steps/10-ifelse/4-check-login/task.md b/1-js/02-first-steps/10-ifelse/4-check-login/task.md new file mode 100644 index 00000000..4b371a1d --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/4-check-login/task.md @@ -0,0 +1,23 @@ +importance: 3 + +--- + +# Check the login + +Write the code which asks for a login with `prompt`. + +If the visitor enters `"Admin"`, then `prompt` for a password, if the input is an empty line or `key:Esc` -- show "Canceled.", if it's another string -- then show "I don't know you". + +The password is checked as follows: + +- If it equals "TheMaster", then show "Welcome!", +- Another string -- show "Wrong password", +- For an empty string or cancelled input, show "Canceled." + +The schema: + +![](ifelse_task.png) + +Please use nested `if` blocks. Mind the overall readability of the code. + +[demo] diff --git a/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/solution.md b/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/solution.md new file mode 100644 index 00000000..638ce81f --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/solution.md @@ -0,0 +1,6 @@ + + +```js +result = (a + b < 4) ? 'Below' : 'Over'; +``` + diff --git a/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md b/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md new file mode 100644 index 00000000..684e239f --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md @@ -0,0 +1,16 @@ +importance: 5 + +--- + +# Rewrite 'if' into '?' + +Rewrite this `if` using the ternary operator `'?'`: + +```js +if (a + b < 4) { + result = 'Below'; +} else { + result = 'Over'; +} +``` + diff --git a/1-js/02-first-steps/10-ifelse/6-rewrite-if-else-question/solution.md b/1-js/02-first-steps/10-ifelse/6-rewrite-if-else-question/solution.md new file mode 100644 index 00000000..6d68e29d --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/6-rewrite-if-else-question/solution.md @@ -0,0 +1,9 @@ + + +```js +let message = (login == 'Employee') ? 'Hello' : + (login == 'Director') ? 'Greetings' : + (login == '') ? 'No login' : + ''; +``` + diff --git a/1-js/02-first-steps/10-ifelse/6-rewrite-if-else-question/task.md b/1-js/02-first-steps/10-ifelse/6-rewrite-if-else-question/task.md new file mode 100644 index 00000000..4f7d994a --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/6-rewrite-if-else-question/task.md @@ -0,0 +1,23 @@ +importance: 5 + +--- + +# Rewrite 'if..else' into '?' + +Rewrite `if..else` using multiple ternary operators `'?'`. + +For readability, it's recommended to split the code into multiple lines. + +```js +let message; + +if (login == 'Employee') { + message = 'Hello'; +} else if (login == 'Director') { + message = 'Greetings'; +} else if (login == '') { + message = 'No login'; +} else { + message = ''; +} +``` diff --git a/1-js/02-first-steps/10-ifelse/article.md b/1-js/02-first-steps/10-ifelse/article.md new file mode 100644 index 00000000..41dd2b9d --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/article.md @@ -0,0 +1,239 @@ +# Conditional operators: if, '?' + +Sometimes we need to perform different actions basing on a condition. + +There's an `if` operator for that and also the "question mark" operator: `"?"` for conditional evaluation. + +[cut] + +## The "if" operator + +The "if" operator gets a condition, evaluates it and -- if the result is `true` -- executes the code. + +For example: + +```js run +let year = prompt('In which year was ECMAScript-2015 specification published?', ''); + +*!* +if (year == 2015) alert( 'You are right!' ); +*/!* +``` + +In the example above, the condition is a simple equality check: `year == 2015`, but it can be much more complex. + +If there's more than one command to execute -- we can use a code block in figure brackets: + +```js +if (year == 2015) { + alert( "That's correct!" ); + alert( "You're so smart!" ); +} +``` + +It is recommended to use figure brackets every time with `if`, even if there's only one command. That improves readability. + +## Boolean conversion + +The `if (…)` operator evaluates the expression in parentheses and converts it to the boolean type. + +Let's recall the conversion rules: + +- A number `0`, an empty string `""`, `null`, `undefined` and `NaN` are `false`, +- Other values -- `true`. + +So, the code under this condition would never execute: + +```js +if (0) { // 0 is falsy + ... +} +``` + +...And inside this condition -- always works: + +```js +if (1) { // 1 is truthy + ... +} +``` + +We can also pass a pre-evaluated boolean value to `if`, like here: + +```js +let cond = (year == 2015); // equality evaluates to true or false + +if (cond) { + ... +} +``` + +## The "else" clause + +The `if` operator may contain an optional "else" block. It executes when the condition is wrong. + +For example: +```js run +let year = prompt('In which year was ECMAScript-2015 specification published?', ''); + +if (year == 2015) { + alert( 'You guessed it right!' ); +} else { + alert( 'How can you be so wrong?' ); // any value except 2015 +} +``` + +## Several conditions: "else if" + +Sometimes we'd like to test several variants of a condition. There's an `else if` clause for that. + +For example: + +```js run +let year = prompt('In which year was ECMAScript-2015 specification published?', ''); + +if (year < 2015) { + alert( 'Too early...' ); +} else if (year > 2015) { + alert( 'Too late' ); +} else { + alert( 'Exactly!' ); +} +``` + +In the code above JavaScript first checks `year < 2015`, if it is falsy then goes to the next condition `year > 2015`, and otherwise shows the last `alert`. + +There can be more `else if` blocks. The ending `else` is optional. + +## Ternary operator '?' + +Sometimes we need to assign a variable depending on a condition. + +For instance: + +```js run no-beautify +let accessAllowed; +let age = prompt('How old are you?', ''); + +*!* +if (age > 18) { + accessAllowed = true; +} else { + accessAllowed = false; +} +*/!* + +alert(accessAllowed); +``` + +The so-called "ternary" or "question mark" operator allows to do that shorter and simpler. + +The operator is represented by a question mark `"?"`. The formal term "ternary" means that the operator has 3 operands. It is actually the one and only operator in JavaScript which has that many. + +The syntax is: +```js +let result = condition ? value1 : value2 +``` + +The `condition` is evaluated, if it's truthy then `value1` is returned, otherwise -- `value2`. + +For example: + +```js +let accessAllowed = (age > 18) ? true : false; +``` + +Technically, we can omit parentheses around `age > 14`. The question mark operator has a low precedence. It executes after the comparison `>`, so that'll do the same: + +```js +// the comparison operator "age > 18" executes first anyway +// (no need to wrap it into parentheses) +let accessAllowed = age > 18 ? true : false; +``` + +...But parentheses make the code more readable. So it's recommended to put them. + +````smart +In the example above it's possible to evade the question mark operator, because the comparison by itself returns `true/false`: + +```js +// the same +let accessAllowed = age > 18; +``` +```` + +## Multiple '?' + +A sequence of question mark `"?"` operators allows to return a value depending on more than one condition. + +For instance: +```js run +let age = prompt('age?', 18); + +let message = (age < 3) ? 'Hi, baby!' : + (age < 18) ? 'Hello!' : + (age < 100) ? 'Greetings!' : + 'What an unusual age!'; + +alert( message ); +``` + +It may be difficult at first to grasp what's going on. But after a closer look we can see that it's just an ordinary sequence of tests. + +1. The first question mark checks whether `age < 3`. +2. If true -- returns `'Hi, baby!'`, otherwise -- goes after the colon `":"` and checks for `age < 18`. +3. If that's true -- returns `'Hello!'`, otherwise -- goes after the next colon `":"` and checks for `age < 100`. +4. If that's true -- returns `'Greetings!'`, otherwise -- goes after the last colon `":"` and returns `'What an unusual age!'`. + +The same logic using `if..else`: + +```js +if (age < 3) { + message = 'Hi, baby!'; +} else if (a < 18) { + message = 'Hello!'; +} else if (age < 100) { + message = 'Greetings!'; +} else { + message = 'What an unusual age!'; +} +``` + +## Non-traditional use of '?' + +Sometimes the question mark `'?'` is used as a replacement for `if`: + +```js run no-beautify +let company = prompt('Which company created JavaScript?', ''); + +*!* +(company == 'Netscape') ? + alert('Right!') : alert('Wrong.'); +*/!* +``` + +Depending on the condition `company == 'Netscape'`, either the first or the second part after `"?"` gets executed and shows the alert. + +We don't assign a result to a variable here, the idea is to execute different code depending on the condition. + +**It is not recommended to use the question mark operator in this way.** + +The notation seem to be shorter than `if`, that appeals to some programmers. But it is less readable. + +Here's the same with `if` for comparison: + +```js run no-beautify +let company = prompt('Which company created JavaScript?', ''); + +*!* +if (company == 'Netscape') { + alert('Right!'); +} else { + alert('Wrong.'); +} +*/!* +``` + +Our eyes scan the code vertically. The constructs which span several lines are easier to understand than a long horizontal instruction set. + +The idea of a question mark `'?'` is to return one or another value depending on the condition. Please use it for exactly that. There's `if` to execute different branches of the code. diff --git a/1-js/02-first-steps/11-logical-operators/1-alert-null-2-undefined/solution.md b/1-js/02-first-steps/11-logical-operators/1-alert-null-2-undefined/solution.md new file mode 100644 index 00000000..8869d32e --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/1-alert-null-2-undefined/solution.md @@ -0,0 +1,6 @@ +The answer is `2`, that's the first truthy value. + +```js run +alert( null || 2 || undefined ); +``` + diff --git a/1-js/02-first-steps/11-logical-operators/1-alert-null-2-undefined/task.md b/1-js/02-first-steps/11-logical-operators/1-alert-null-2-undefined/task.md new file mode 100644 index 00000000..eda8c905 --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/1-alert-null-2-undefined/task.md @@ -0,0 +1,12 @@ +importance: 5 + +--- + +# What's the result of OR? + +What the code below is going to output? + +```js +alert( null || 2 || undefined ); +``` + diff --git a/1-js/02-first-steps/11-logical-operators/2-alert-or/solution.md b/1-js/02-first-steps/11-logical-operators/2-alert-or/solution.md new file mode 100644 index 00000000..8f4d664e --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/2-alert-or/solution.md @@ -0,0 +1,13 @@ +The answer: first `1`, then `2`. + +```js run +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`. +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. + +There will be no `3`, because the evaluation does not reach `alert(3)`. diff --git a/1-js/02-first-steps/11-logical-operators/2-alert-or/task.md b/1-js/02-first-steps/11-logical-operators/2-alert-or/task.md new file mode 100644 index 00000000..bc622abf --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/2-alert-or/task.md @@ -0,0 +1,12 @@ +importance: 3 + +--- + +# What's the result of OR'ed alerts? + +What the code below will output? + +```js +alert( alert(1) || 2 || alert(3) ); +``` + diff --git a/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/solution.md b/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/solution.md new file mode 100644 index 00000000..5c2455ef --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/solution.md @@ -0,0 +1,6 @@ +The answer: `null`, because it's the first falsy value from the list. + +```js run +alert( 1 && null && 2 ); +``` + diff --git a/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/task.md b/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/task.md new file mode 100644 index 00000000..53ec7874 --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/task.md @@ -0,0 +1,12 @@ +importance: 5 + +--- + +# What is the result of AND? + +What this code is going to show? + +```js +alert( 1 && null && 2 ); +``` + diff --git a/1-js/02-first-steps/11-logical-operators/4-alert-and/solution.md b/1-js/02-first-steps/11-logical-operators/4-alert-and/solution.md new file mode 100644 index 00000000..b6fb10d7 --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/4-alert-and/solution.md @@ -0,0 +1,10 @@ +The answer: `1`, and then `undefined`. + +```js run +alert( alert(1) && alert(2) ); +``` + +The call to `alert` returns `undefined` (it just shows a message, so there's no meaningful return). + +Because of that, `&&` evaluates the left operand (outputs `1`), and immediately stops, because `undefined` is a falsy value. And `&&` looks for a falsy value and returns it, so it's done. + diff --git a/1-js/02-first-steps/11-logical-operators/4-alert-and/task.md b/1-js/02-first-steps/11-logical-operators/4-alert-and/task.md new file mode 100644 index 00000000..69f877b9 --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/4-alert-and/task.md @@ -0,0 +1,12 @@ +importance: 3 + +--- + +# What is the result of AND'ed alerts? + +What will this code show? + +```js +alert( alert(1) && alert(2) ); +``` + diff --git a/1-js/02-first-steps/11-logical-operators/5-alert-and-or/solution.md b/1-js/02-first-steps/11-logical-operators/5-alert-and-or/solution.md new file mode 100644 index 00000000..32a8ccf2 --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/5-alert-and-or/solution.md @@ -0,0 +1,16 @@ +The answer: `3`. + +```js run +alert( null || 2 && 3 || 4 ); +``` + +The precedence of AND `&&` is higher than `||`, so it executes first. + +The result of `2 && 3 = 3`, so the expression becomes: + +``` +null || 3 || 4 +``` + +Now the result if the first truthy value: `3`. + diff --git a/1-js/02-first-steps/11-logical-operators/5-alert-and-or/task.md b/1-js/02-first-steps/11-logical-operators/5-alert-and-or/task.md new file mode 100644 index 00000000..4b2ad046 --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/5-alert-and-or/task.md @@ -0,0 +1,12 @@ +importance: 5 + +--- + +# The result of OR AND OR + +What will be the result? + +```js +alert( null || 2 && 3 || 4 ); +``` + diff --git a/1-js/2-first-steps/13-logical-ops/6-check-if-in-range/solution.md b/1-js/02-first-steps/11-logical-operators/6-check-if-in-range/solution.md similarity index 100% rename from 1-js/2-first-steps/13-logical-ops/6-check-if-in-range/solution.md rename to 1-js/02-first-steps/11-logical-operators/6-check-if-in-range/solution.md diff --git a/1-js/02-first-steps/11-logical-operators/6-check-if-in-range/task.md b/1-js/02-first-steps/11-logical-operators/6-check-if-in-range/task.md new file mode 100644 index 00000000..cc00ca9f --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/6-check-if-in-range/task.md @@ -0,0 +1,9 @@ +importance: 3 + +--- + +# Check the range between + +Write an "if" condition to check that `age` is between `14` and `90` inclusively. + +"Inclusively" means that `age` can reach the edges `14` or `90`. diff --git a/1-js/02-first-steps/11-logical-operators/7-check-if-out-range/solution.md b/1-js/02-first-steps/11-logical-operators/7-check-if-out-range/solution.md new file mode 100644 index 00000000..d1946a96 --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/7-check-if-out-range/solution.md @@ -0,0 +1,12 @@ +The first variant: + +```js +if (!(age >= 14 && age <= 90)) +``` + +The second variant: + +```js +if (age < 14 || age > 90) +``` + diff --git a/1-js/02-first-steps/11-logical-operators/7-check-if-out-range/task.md b/1-js/02-first-steps/11-logical-operators/7-check-if-out-range/task.md new file mode 100644 index 00000000..7c22d6ad --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/7-check-if-out-range/task.md @@ -0,0 +1,9 @@ +importance: 3 + +--- + +# Check the range outside + +Write an `if` condition to check that `age` is NOT between 14 and 90 inclusively. + +Create two variants: the first one using NOT `!`, the second one -- without it. diff --git a/1-js/02-first-steps/11-logical-operators/8-if-question/solution.md b/1-js/02-first-steps/11-logical-operators/8-if-question/solution.md new file mode 100644 index 00000000..21050975 --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/8-if-question/solution.md @@ -0,0 +1,20 @@ +The answer: the first and the third will execute. + +Details: + +```js run +// Runs. +// The result of -1 || 0 = -1, truthy +if (-1 || 0) alert( 'first' ); + +// Doesn't run +// -1 && 0 = 0, falsy +if (-1 && 0) alert( 'second' ); + +// Executes +// Operator && has a higher precedence than || +// so -1 && 1 executes first, giving us the chain: +// null || -1 && 1 -> null || 1 -> 1 +if (null || -1 && 1) alert( 'third' ); +``` + diff --git a/1-js/02-first-steps/11-logical-operators/8-if-question/task.md b/1-js/02-first-steps/11-logical-operators/8-if-question/task.md new file mode 100644 index 00000000..f824779b --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/8-if-question/task.md @@ -0,0 +1,16 @@ +importance: 5 + +--- + +# A question about "if" + +Which of these `alert`s are going to execute? + +What will be the results of the expressions inside `if(...)`? + +```js +if (-1 || 0) alert( 'first' ); +if (-1 && 0) alert( 'second' ); +if (null || -1 && 1) alert( 'third' ); +``` + diff --git a/1-js/02-first-steps/11-logical-operators/article.md b/1-js/02-first-steps/11-logical-operators/article.md new file mode 100644 index 00000000..e36f9455 --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/article.md @@ -0,0 +1,307 @@ +# Logical operators + +There are three logical operators in JavaScript: `||` (OR), `&&` (AND), `!` (NOT). + +Although they are called "logical", they can be applied to values of any type, not only boolean. The result can also be of any type. + +Let's see the details. + +[cut] + +## || (OR) + +The "OR" operator is represented with two vertical line symbols: + +```js +result = a || b; +``` + +In classical programming, logical OR is meant to manipulate boolean values. If any of it's arguments is `true`, then it returns `true`, otherwise -- returns `false`. + +In JavaScript the operator is a little bit more tricky and powerful. But first let's see what happens with boolean values. + +There are four possible logical combinations: + +```js run +alert( true || true ); // true +alert( false || true ); // true +alert( true || false ); // true +alert( false || false ); // false +``` + +As we can see, the result is always `true` except for the case when both operands are `false`. + +If an operand is not boolean, then it's converted to boolean for the evaluation. + +For instance, a number `1` is treated as `true`, a number `0` -- as `false`: + +```js run +if (1 || 0) { // works just like if( true || false ) + alert( 'truthy!' ); +} +``` + +Most of time, OR `||` is used in `if` to test if *any* of given conditions is correct. + +For example: + +```js run +let hour = 9; + +*!* +if (hour < 10 || hour > 18) { +*/!* + alert( 'The office is closed.' ); +} +``` + +We can pass more conditions: + +```js run +let hour = 12; +let isWeekend = true; + +if (hour < 10 || hour > 18 || isWeekend) { + alert( 'The office is closed.' ); // it is weekend +} +``` + +## OR seeks the first truthy value + +The logic described above is somewhat classical. Now let's bring in the "extra" features of JavaScipt. + +The extended algorithm works as follows. + +Given multiple OR'ed values: + +```js +result = value1 || value2 || value3; +``` + +The OR `"||"` operator is doing the following: + +- Evalutes operands from left to right. +- For each value -- converts it to boolean. If it's true then stops and returns that value. +- If operands finished, returns the last value. + +A value is returned in it's original form, without the conversion. + +In other words, a chain of OR `"||"` returns the first truthy value or the last one if no such value found. + +For instance: + +```js run +alert( 1 || 0 ); // 1 (1 is truthy) +alert( true || 'no matter what' ); // (true is truthy) + +alert( null || 1 ); // 1 (1 is the first truthy value) +alert( null || 0 || 1 ); // 1 (the first truthy value) +alert( undefined || null || 0 ); // 0 (all falsy, returns the last value) +``` + +That leads to some interesting usages compared to a "pure, classical, boolean-only OR". + +1. **Getting the first truthy value from the list of variables or expressions.** + + Imagine we have several variables, which can either contain the data or be `null/undefined`. And we need to choose the first one with data. + + We can use OR `||` for that: + + ```js run + let currentUser = null; + let defaultUser = "John"; + + *!* + let name = currentUser || defaultUser || "unnamed"; + */!* + + alert( name ); // selects "John" – the first truthy value + ``` + + If both `currentUser` and `defaultUser` were falsy then `"unnamed"` would be the result. +2. **Short-circuit evaluation.** + + Operands can be not only values, but arbitrary expressions. OR evaluates and tests them from left to right. The evaluation stops when a truthy value is reached, and the value is returned. The process is called "a short-circuit evaluation", because it goes as short as possible from left to right. + + This is clearly seen when the expression given as the second argument has a side effect. Like a variable assignment. + + If we run the example below, `x` would not get assigned: + + ```js run no-beautify + let x; + + *!*true*/!* || (x = 1); + + alert(x); // undefined, because (x = 1) not evaluated + ``` + + ...And if the first argument is `false`, then `OR` goes on and evaluates the second one thus running the assignment: + + ```js run no-beautify + let x; + + *!*false*/!* || (x = 1); + + alert(x); // 1 + ``` + + An assignment is a simple case, other side effects can be involved. + + As we can see, such use case is a "shorter way to do `if`". The first operand is converted to boolean and if it's false then the second one is evaluated. + + Most of time it's better to use a "regular" `if` to keep the code easy to understand, but sometimes that can be handy. + +## && (AND) + +The AND operator is represented with two ampersands `&&`: + +```js +result = a && b; +``` + +In classical programming AND returns `true` if both operands are truthy and `false` -- otherwise: + +```js run +alert( true && true ); // true +alert( false && true ); // false +alert( true && false ); // false +alert( false && false ); // false +``` + +An example with `if`: + +```js run +let hour = 12; +let minute = 30; + +if (hour == 12 && minute == 30) { + alert( 'Time is 12:30' ); +} +``` + +Just as for OR, any value is allowed as an operand of AND: + +```js run +if (1 && 0) { // evaluated as true && false + alert( "won't work, because the result is falsy" ); +} +``` + + +## AND seeks the first falsy value + +Given multiple AND'ed values: + +```js +result = value1 && value2 && value3; +``` + +The AND `"&&"` operator is doing the following: + +- Evalutes operands from left to right. +- For each value converts it to a boolean. If the result is `false`, stops and returns the original value. +- If values finished (all are truthy), returns the last one. + +In other words, AND returns the first falsy value or the last value if none found. + +The rules above are similar to OR. The difference is that AND returns the first *falsy* value while OR returns the first *truthy* one. + +Examples: + +```js run +// if the first operand is truthy, +// AND returns the second one: +alert( 1 && 0 ); // 0 +alert( 1 && 5 ); // 5 + +// if the first operand is falsy, +// AND returns it, and the second one is ignored +alert( null && 5 ); // null +alert( 0 && "no matter what" ); // 0 +``` + +We can also pass several values in a row. See how the first falsy one is returned: + +```js run +alert( 1 && 2 && null && 3 ); // null +``` + +When all values are truthy, the last value is returned: + +```js run +alert( 1 && 2 && 3 ); // 3, the last one +``` + +````smart header="AND `&&` executes before OR `||`" +The precedence of the AND `&&` operator is higher than OR `||`, so it executes before OR. + +In the code below `1 && 0` is calculated first: + +```js run +alert( 5 || 1 && 0 ); // 5 +``` +```` + +Just like OR, the AND `&&` operator can sometimes replace `if`. + +For instance: + +```js run +let x = 1; + +(x > 0) && alert( 'Greater than zero!' ); +``` + +The action in the right part of `&&` would execute only if the evaluation reaches it. That is: only if `(x > 0)` is true. + +So we basically have an analogue for: + +```js run +let x = 1; + +if (x > 0) { + alert( 'Greater than zero!' ); +} +``` + +The variant with `&&` appears to be shorter. But `if` is more obvious and tends to be a little bit more readable. + +So it is recommended to use every construct for it's purpose. Use `if` if we want if. And use `&&` if we want AND. + +## ! (NOT) + +The boolean NOT operator is represented with an exclamation sign `"!"`. + +The syntax is pretty simple: + +```js +result = !value; +``` + +The operator accepts a single argument and does the following: + +1. Converts the operand to boolean type: `true/false`. +2. Returns an inverse value. + +For instance: + +```js run +alert( !true ); // false +alert( !0 ); // true +``` + +A double NOT `!!` is sometimes used for converting a value to boolean type: + +```js run +alert( !!"non-empty string" ); // true +alert( !!null ); // false +``` + +That is: the first NOT converts the value to boolean and returns the inverse, and the second NOT inverses it again, at the end we have a plain value-to-boolean conversion. + +There's a little more verbose to do the same -- a built-in `Boolean` function: + +```js run +alert( Boolean("non-empty string") ); // true +alert( Boolean(null) ); // false +``` diff --git a/1-js/02-first-steps/12-while-for/1-loop-last-value/solution.md b/1-js/02-first-steps/12-while-for/1-loop-last-value/solution.md new file mode 100644 index 00000000..43ee4aad --- /dev/null +++ b/1-js/02-first-steps/12-while-for/1-loop-last-value/solution.md @@ -0,0 +1,25 @@ +The answer: `1`. + +```js run +let i = 3; + +while (i) { + alert( i-- ); +} +``` + +Every loop iteration decreases `i` by `1`. The check `while(i)` stops the loop when `i = 0`. + +Hence, the steps of the loop form the following sequence ("loop unrolled"): + +```js +let i = 3; + +alert(i--); // shows 3, decreases i to 2 + +alert(i--) // shows 2, decreases i to 1 + +alert(i--) // shows 1, decreases i to 0 + +// done, while(i) check stops the loop +``` diff --git a/1-js/02-first-steps/12-while-for/1-loop-last-value/task.md b/1-js/02-first-steps/12-while-for/1-loop-last-value/task.md new file mode 100644 index 00000000..3b847dfa --- /dev/null +++ b/1-js/02-first-steps/12-while-for/1-loop-last-value/task.md @@ -0,0 +1,15 @@ +importance: 3 + +--- + +# Last loop value + +What is the last value alerted by this code? Why? + +```js +let i = 3; + +while (i) { + alert( i-- ); +} +``` diff --git a/1-js/02-first-steps/12-while-for/2-which-value-while/solution.md b/1-js/02-first-steps/12-while-for/2-which-value-while/solution.md new file mode 100644 index 00000000..add79c87 --- /dev/null +++ b/1-js/02-first-steps/12-while-for/2-which-value-while/solution.md @@ -0,0 +1,30 @@ +The task demonstrates how postfix/prefix forms can lead to different results when used in comparisons. + +1. **From 1 to 4** + + ```js run + let i = 0; + while (++i < 5) alert( i ); + ``` + + The first value is `i=1`, because `++i` first increments `i` and then returns the new value. So the first comparison is `1 < 5` and the `alert` shows `1`. + + Then follow `2,3,4…` -- the values show up one after another. The comparison always uses the incremented value, because `++` is before the variable. + + Finally, `i=4` is incremented to `5`, the comparison `while(5 < 5)` fails, and the loop stops. So `5` is not shown. +2. **From 1 to 5** + + ```js run + let i = 0; + while (i++ < 5) alert( i ); + ``` + + The first value is again `i=1`. The postfix form of `i++` increments `i` and then returns the *old* value, so the comparison `i++ < 5` will use `i=0` (contrary to `++i < 5`). + + But the `alert` call is separate. It's another statement which executes after the increment and the comparison. So it gets the current `i=1`. + + Then follow `2,3,4…` + + Let's stop on `i=4`. The prefix form `++i` would increment it and use `5` in the comparison. But here we have the postfix form `i++`. So it increments `i` to `5`, but returns the old value. Hence the comparison is actually `while(4 < 5)` -- true, and the control goes on to `alert`. + + The value `i=5` is the last one, because on the next step `while(5 < 5)` is false. diff --git a/1-js/02-first-steps/12-while-for/2-which-value-while/task.md b/1-js/02-first-steps/12-while-for/2-which-value-while/task.md new file mode 100644 index 00000000..4576db35 --- /dev/null +++ b/1-js/02-first-steps/12-while-for/2-which-value-while/task.md @@ -0,0 +1,22 @@ +importance: 4 + +--- + +# Which values shows the while? + +For every loop, write down which values it shows, in your opinion. And then compare with the answer. + +Both loops `alert` same values or not? + +1. The prefix form `++i`: + + ```js + let i = 0; + while (++i < 5) alert( i ); + ``` +2. The postfix form `i++` + + ```js + let i = 0; + while (i++ < 5) alert( i ); + ``` diff --git a/1-js/02-first-steps/12-while-for/3-which-value-for/solution.md b/1-js/02-first-steps/12-while-for/3-which-value-for/solution.md new file mode 100644 index 00000000..766278fd --- /dev/null +++ b/1-js/02-first-steps/12-while-for/3-which-value-for/solution.md @@ -0,0 +1,17 @@ +**The answer: from `0` to `4` in both cases.** + +```js run +for (let i = 0; i < 5; ++i) alert( i ); + +for (let i = 0; i < 5; i++) alert( i ); +``` + +That can be easily deducted from the algorithm of `for`: + +1. Execute once `i=0` before everything (begin). +2. Check the condition `i<5` +3. If `true` -- execute the loop body `alert(i)`, and then `i++` + +The increment `i++` is separated from the condition check (2). That's just another statement. + +The value returned by the increment is not used here, so there's no difference between `i++` and `++i`. diff --git a/1-js/02-first-steps/12-while-for/3-which-value-for/task.md b/1-js/02-first-steps/12-while-for/3-which-value-for/task.md new file mode 100644 index 00000000..bfefa63f --- /dev/null +++ b/1-js/02-first-steps/12-while-for/3-which-value-for/task.md @@ -0,0 +1,20 @@ +importance: 4 + +--- + +# Which values get shown by the "for" loop? + +For each loop write down which values it is going to show. Then compare with the answer. + +Both loops `alert` same values or not? + +1. The postfix form: + + ```js + for (let i = 0; i < 5; i++) alert( i ); + ``` +2. The prefix form: + + ```js + for (let i = 0; i < 5; ++i) alert( i ); + ``` diff --git a/1-js/02-first-steps/12-while-for/4-for-even/solution.md b/1-js/02-first-steps/12-while-for/4-for-even/solution.md new file mode 100644 index 00000000..e8e66bb4 --- /dev/null +++ b/1-js/02-first-steps/12-while-for/4-for-even/solution.md @@ -0,0 +1,11 @@ + + +```js run demo +for (let i = 2; i <= 10; i++) { + if (i % 2 == 0) { + alert( i ); + } +} +``` + +We use the "modulo" operator `%` to get the remainder and check for the evenness here. diff --git a/1-js/02-first-steps/12-while-for/4-for-even/task.md b/1-js/02-first-steps/12-while-for/4-for-even/task.md new file mode 100644 index 00000000..ff34e7e4 --- /dev/null +++ b/1-js/02-first-steps/12-while-for/4-for-even/task.md @@ -0,0 +1,9 @@ +importance: 5 + +--- + +# Output even numbers in the loop + +Use the `for` loop to output even numbers from `2` to `10`. + +[demo] diff --git a/1-js/02-first-steps/12-while-for/5-replace-for-while/solution.md b/1-js/02-first-steps/12-while-for/5-replace-for-while/solution.md new file mode 100644 index 00000000..612cf559 --- /dev/null +++ b/1-js/02-first-steps/12-while-for/5-replace-for-while/solution.md @@ -0,0 +1,10 @@ + + +```js run +let i = 0; +while (i < 3) { + alert( `number ${i}!` ); + i++; +} +``` + diff --git a/1-js/02-first-steps/12-while-for/5-replace-for-while/task.md b/1-js/02-first-steps/12-while-for/5-replace-for-while/task.md new file mode 100644 index 00000000..a62c9af3 --- /dev/null +++ b/1-js/02-first-steps/12-while-for/5-replace-for-while/task.md @@ -0,0 +1,14 @@ +importance: 5 + +--- + +# Replace "for" with "while" + +Rewrite the code changing the `for` loop to `while` without altering it's behavior (the output should stay same). + +```js run +for (let i = 0; i < 3; i++) { + alert( `number ${i}!` ); +} +``` + diff --git a/1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md b/1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md new file mode 100644 index 00000000..344824e0 --- /dev/null +++ b/1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md @@ -0,0 +1,15 @@ + +```js run demo +let num; + +do { + num = prompt("Enter a number greater than 100?", 0); +} while (num <= 100 && num); +``` + +The loop `do..while` repeats while both checks are truthy: + +1. The check for `num <= 100` -- that is, the entered value is still not greater than `100`. +2. The check `&& num` is false when `num` is `null` or a empty strig. Then the `while` loop stops too. + +P.S. If `num` is `null` then `num <= 100` is `false`, so without the 2nd check the loop wouldn't stop if the user clicks CANCEL. Both checks are required. diff --git a/1-js/02-first-steps/12-while-for/6-repeat-until-correct/task.md b/1-js/02-first-steps/12-while-for/6-repeat-until-correct/task.md new file mode 100644 index 00000000..e1fe0db4 --- /dev/null +++ b/1-js/02-first-steps/12-while-for/6-repeat-until-correct/task.md @@ -0,0 +1,13 @@ +importance: 5 + +--- + +# Repeat until the input is incorrect + +Write a loop which prompts for a number greater than `100`. If the visitor enters another number -- ask him to input again. + +The loop must ask for a number until either the visitor enters a number greater than `100` or cancels the input/enters an empty line. + +Here we can assume that the visitor only inputs numbers. There's no need to implement a special handling for a non-numeric input in this task. + +[demo] diff --git a/1-js/02-first-steps/12-while-for/7-list-primes/solution.md b/1-js/02-first-steps/12-while-for/7-list-primes/solution.md new file mode 100644 index 00000000..ec906317 --- /dev/null +++ b/1-js/02-first-steps/12-while-for/7-list-primes/solution.md @@ -0,0 +1,29 @@ +There are many algorithms for this task. + +Let's use a nested loop: + +```js +For each i in the interval { + check if i has a divisor from 1..i + if yes => the value is not a prime + if no => the value is a prime, show it +} +``` + +The code using a label: + +```js run +let n = 10; + +nextPrime: +for (let i = 2; i <= n; i++) { // for each i... + + for (let j = 2; j < i; j++) { // look for a divisor.. + if (i % j == 0) continue nextPrime; // not a prime, go next i + } + + alert( i ); // a prime +} +``` + +There's a lot of space to opimize it. For instance, we could look for the divisors from `2` to square root of `i`. But anyway, if we want to be really efficient for large intervals, we need change the approach and rely on advanced maths and complex algorithms like [Quadratic sieve](https://en.wikipedia.org/wiki/Quadratic_sieve), [General number field sieve](https://en.wikipedia.org/wiki/General_number_field_sieve) etc. diff --git a/1-js/02-first-steps/12-while-for/7-list-primes/task.md b/1-js/02-first-steps/12-while-for/7-list-primes/task.md new file mode 100644 index 00000000..0f92fecf --- /dev/null +++ b/1-js/02-first-steps/12-while-for/7-list-primes/task.md @@ -0,0 +1,17 @@ +importance: 3 + +--- + +# Output prime numbers + +An integer number greater than `1` is called a [prime](https://en.wikipedia.org/wiki/Prime_number) if it cannot be divided without a remainder by anything except `1` and itself. + +In other words, `n>1` is a prime if it can't be evenly divided by anything except `1` and `n`. + +For example, `5` is a prime, because it cannot be divided without a remainder by `2`, `3` and `4`. + +**Write the code which outputs prime numbers in the interval from `2` to `n`.** + +For `n=10` the result will be `2,3,5,7`. + +P.S. The code should work for any `n`, not be hard-tuned for any fixed value. diff --git a/1-js/02-first-steps/12-while-for/article.md b/1-js/02-first-steps/12-while-for/article.md new file mode 100644 index 00000000..575cfa68 --- /dev/null +++ b/1-js/02-first-steps/12-while-for/article.md @@ -0,0 +1,390 @@ +# Loops: while and for + +We often have a need to perform similar actions many times in a row. + +For example, when we need to output goods from the list one after another. Or just run the same code for each number from 1 to 10. + +*Loops* are a way to repeat the same part of code multiple times. + +[cut] + +## The "while" loop + +The `while` loop has the following syntax: + +```js +while (condition) { + // code + // so-called "loop body" +} +``` + +While the `condition` is `true` -- the `code` from the loop body is executed. + +For instance, the loop below outputs `i` while `i<3`: + +```js run +let i = 0; +while (i < 3) { // shows 0, then 1, then 2 + alert( i ); + i++; +} +``` + +A single execution of the loop body is called *an iteration*. The loop in the example above makes 3 iterations. + +If there were no `i++` in the example above, the loop would repeat (in theory) forever. In practice, the browser provides ways to stop such loops, and for server-side JavaScript we can kill the process. + +Any expression or a variable can be a loop condition, not just a comparison. They are evaluated and converted to boolean by `while`. + +For instance, the shorter way to write `while (i!=0)` could be `while (i)`: + +```js run +let i = 3; +*!* +while (i) { // when i becomes 0, the condition becomes falsy, and the loop stops +*/!* + alert( i ); + i--; +} +``` + +````smart header="Brackets are not required for a single-line body" +If the loop body has a single statement, we can omit the brackets `{…}`: + +```js run +let i = 3; +*!* +while (i) alert(i--); +*/!* +``` +```` + +## The "do..while" loop + +The condition check can be moved *below* the loop body using the `do..while` syntax: + +```js +do { + // loop body +} while (condition); +``` + +The loop will first execute the body, then check the condition, and while it's truthy -- execute it again and again. + +For example: + +```js run +let i = 0; +do { + alert( i ); + i++; +} while (i < 3); +``` + +This form of syntax is rarely used. Usually, if there's no special reason, the other form is preferred: `while(…) {…}`. + +## The "for" loop + +The `for` loop is the most often used one. + +It looks like this: + +```js +for (begin; condition; step) { + // ... loop body ... +} +``` + +Let's learn the meaning of these parts by example. The loop below runs `alert(i)` for `i` from `0` up to (but not including) `3`: + +```js run +for (let i = 0; i < 3; i++) { // shows 0, then 1, then 2 + alert(i); +} +``` + +Let's examine the `for` statement part by part: + +| part | | | +|-------|----------|----------------------------------------------------------------------------| +| begin | `i=0` | Executes once upon entering the loop. | +| condition | `i<3`| Checked before every loop iteration, if fails the loop stops. | +| step| `i++` | Executes after the body on each iteration, but before the condition check. | +| body | `alert(i)`| Runs again and again while the condition is truthy | + + +The general loop algorithm works like this: +``` +Run begin +→ (if condition → run body and run step) +→ (if condition → run body and run step) +→ (if condition → run body and run step) +→ ... +``` + +If you are new to loops, then maybe it would help if you go back to the example and reproduce how it runs step-by-step on a piece of paper. + +Here's what exactly happens in our case: + +```js +// for (let i = 0; i < 3; i++) alert(i) + +// run begin +let i = 0 +// if condition → run body and run step +if (i < 3) { alert(i); i++ } +// if condition → run body and run step +if (i < 3) { alert(i); i++ } +// if condition → run body and run step +if (i < 3) { alert(i); i++ } +// ...finish, because now i == 3 +``` + +````smart header="Inline variable declaration" +Here the "counter" variable `i` is declared right in the loop. That's called an "inline" variable declaration. Such variable is visible only inside the loop. + +```js run +for (*!*let*/!* i = 0; i < 3; i++) { + alert(i); // 0, 1, 2 +} +alert(i); // error, no such variable +``` + +Instead of defining a variable, we can use an existing one: + +```js run +let i = 0; + +for (i = 0; i < 3; i++) { // use an existing variable + alert(i); // 0, 1, 2 +} + +alert(i); // 3, visible, because declared outside of the loop +``` + +```` + + +### Skipping parts + +Any part of `for` can be skipped. + +For example, we can omit `begin` if we don't need to do anything at the loop start. + +Like here: + +```js run +let i = 0; // we have i already declared and assigned + +for (; i < 3; i++) { // no need for "begin" + alert( i ); // 0, 1, 2 +} +``` + +We can also remove the `step` part: + +```js run +let i = 0; + +for (; i < 3;) { + alert( i ); +} +``` + +The loop became identical to `while (i<3)`. + +We can actually remove everything, thus creating an infinite loop: + +```js +for (;;) { + // repeats without limits +} +``` + +Please note that the two `for` semicolons `;` must present, otherwise it would be a syntax error. + +## Breaking the loop + +Normally the loop exits when the condition becomes falsy. + +But we can force the exit at any moment. There's a special `break` directive for that. + +For example, this code below asks user for numbers and breaks if no number entered: + +```js +let sum = 0; + +while (true) { + + let value = +prompt("Enter a number", ''); + +*!* + if (!value) break; // (*) +*/!* + + sum += value; + +} +alert( 'Sum: ' + sum ); +``` + +The `break` directive is activated in the line `(*)` if the user enters an empty line or cancels the input. It stops the loop immediately, passing the control to the first line after the loop. Namely, `alert`. + +The combination: "infinite loop + `break` as needed" is great for situations when the condition must be checked not in beginning/end of the loop, but in the middle. Or even in several places of the body. + +## Continue to the next iteration [#continue] + +The `continue` directive is a "lighter version" of `break`. It doesn't stop the whole loop. Instead if stops the current iteration and forces the loop to start a new one (if the condition allows). + +We can use it if we're done on the current iteration and would like to move on to the next. + +The loop above uses `continue` to output only odd values: + +```js run no-beautify +for (let i = 0; i < 10; i++) { + + // if true, skip the remaining part of the body + *!*if (i % 2 == 0) continue;*/!* + + alert(i); // 1, then 3, 5, 7, 9 +} +``` + +For even values of `i` the `continue` directive stops body execution, passing the control to the next iteration of `for` (with the next number). So the `alert` is only called for odd values. + +````smart header="The directive `continue` helps to decrease nesting level" +A loop that shows odd values could look like this: + +```js +for (let i = 0; i < 10; i++) { + + if (i % 2) { + alert( i ); + } + +} +``` + +From the technical point of view it's identical to the example above. Surely, we can just wrap the code in the `if` block instead of `continue`. + +But as a side-effect we got one more figure brackets nesting level. If the code inside `if` is longer than a few lines, that may decrease the overall readability. +```` + +````warn header="No `break/continue` to the right side of '?'" +Please note that syntax constructs that are not expressions cannot be used in `'?'`. In particular, directives `break/continue` are disallowed there. + +For example, if we take this code: + +```js +if (i > 5) { + alert(i); +} else { + continue; +} +``` + +...And rewrite it using a question mark: + + +```js no-beautify +(i > 5) ? alert(i) : *!*continue*/!*; // continue not allowed here +``` + +...Then it stops working. The code like this will give a syntax error: + + +That's just another reason not to use a question mark operator `'?'` instead of `if`. +```` + +## Labels for break/continue + +Sometimes we need to break out from multiple nested loops at once. + +For example, in the code below we loop over `i` and `j` prompting for coordinates `(i, j)` from `(0,0)` to `(3,3)`: + +```js run no-beautify +for (let i = 0; i < 3; i++) { + + for (let j = 0; j < 3; j++) { + + let input = prompt(`Value at coords (${i},${j})`, ''); + + // what if I want to exit from here to Done (below)? + + } +} + +alert('Done!'); +``` + +We need a way to stop the process if the user cancels the input. + +The ordinary `break` after `input` would only break the inner loop. That's not sufficient. Labels come to the rescue. + +A *label* is an identifier with a colon before a loop: +```js +labelName: for(...) { + ... +} +``` + +The `break ` statement in the loop breaks out to the label. + +Like here: + +```js run no-beautify +*!*outer:*/!* for (let i = 0; i < 3; i++) { + + for (let j = 0; j < 3; j++) { + + let input = prompt(`Value at coords (${i},${j})`, ''); + + // if an empty string or canceled, then break out of both loops + if (!input) *!*break outer*/!*; // (*) + + // do something with the value... + } +} +alert('Done!'); +``` + +In the code above `break outer` looks upwards for the label named `outer` and breaks out of that loop. + +So the control goes straight from `(*)` to `alert('Done!')`. + +We can also move a label into the separate string: + +```js no-beautify +outer: +for (let i = 0; i < 3; i++) { ... } +``` + +The `continue` directive can also be used with a label. In this case the execution jumps to the next iteration of the labelled loop. + +````warn header="Labels are not a \"goto\"" +Labels do not allow to jump into an arbitrary place of code. + +For example, it is impossible to do like this: +```js +break label; // jumps to label? No. + +label: for(...) +``` + +The call to a `break/continue` is only possible from inside the loop, and the label must be somewhere upwards from the directive. +```` + +## Summary + +We covered 3 types of loops: + +- `while` -- the condition is checked before each iteration. +- `do..while` -- the condition is checked after each iteration. +- `for(;;)` -- the condition is checked before each iteration, additional settings available. + +To make an "infinite" loop, usually the `while(true)` construct is used. Such a loop, just like any other, can be stopped with the `break` directive. + +If we don't want to do anything on the current iteration and would like to forward to the next one -- the `continue` directive does it. + +`Break/continue` support labels before the loop. A label is the only way for `break/continue` to escape the nesting and go to the outer loop. diff --git a/1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/solution.md b/1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/solution.md new file mode 100644 index 00000000..d3e39743 --- /dev/null +++ b/1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/solution.md @@ -0,0 +1,20 @@ +To precisely match the functionality of `switch`, the `if` must use a strict comparison `'==='`. + +For given strings though, a simple `'=='` works too. + +```js no-beautify +if(browser == 'Edge') { + alert("You've got the Edge!"); +} else if (browser == 'Chrome' + || browser == 'Firefox' + || browser == 'Safari' + || browser == 'Opera') { + alert( 'Okay we support these browsers too' ); +} else { + alert( 'We hope that this page looks ok!' ); +} +``` + +Please note: the construct `browser == 'Chrome' || browser == 'Firefox' …` is split into multiple lines for better readability. + +But the `switch` construct is still cleaner and more descriptive. diff --git a/1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/task.md b/1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/task.md new file mode 100644 index 00000000..f4dc0e5f --- /dev/null +++ b/1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/task.md @@ -0,0 +1,26 @@ +importance: 5 + +--- + +# Rewrite the "switch" into an "if" + +Write the code using `if..else` which would correspond to the following `switch`: + +```js +switch (browser) { + case 'Edge': + alert( "You've got the Edge!" ); + break; + + case 'Chrome': + case 'Firefox': + case 'Safari': + case 'Opera': + alert( 'Okay we support these browsers too' ); + break; + + default: + alert( 'We hope that this page looks ok!' ); +} +``` + diff --git a/1-js/02-first-steps/13-switch/2-rewrite-if-switch/solution.md b/1-js/02-first-steps/13-switch/2-rewrite-if-switch/solution.md new file mode 100644 index 00000000..ed87dd94 --- /dev/null +++ b/1-js/02-first-steps/13-switch/2-rewrite-if-switch/solution.md @@ -0,0 +1,26 @@ +The first two checks turn into two `case`. The third check is split into two cases: + +```js run +let a = +prompt('a?', ''); + +switch (a) { + case 0: + alert( 0 ); + break; + + case 1: + alert( 1 ); + break; + + case 2: + case 3: + alert( '2,3' ); +*!* + break; +*/!* +} +``` + +Please note: the `break` at the bottom is not required. But we put it to make the code future-proof. + +In the future, there is a chance that we'd want to add one more `case`, for example `case 4`. And if we forget to add a break before it, at the end of `case 3`, there will be an error. So that's a kind of self-insurance. diff --git a/1-js/02-first-steps/13-switch/2-rewrite-if-switch/task.md b/1-js/02-first-steps/13-switch/2-rewrite-if-switch/task.md new file mode 100644 index 00000000..ec99d098 --- /dev/null +++ b/1-js/02-first-steps/13-switch/2-rewrite-if-switch/task.md @@ -0,0 +1,23 @@ +importance: 4 + +--- + +# Rewrite "if" into "switch" + +Rewrite the code below using a single `switch` statement: + +```js run +let a = +prompt('a?', ''); + +if (a == 0) { + alert( 0 ); +} +if (a == 1) { + alert( 1 ); +} + +if (a == 2 || a == 3) { + alert( '2,3' ); +} +``` + diff --git a/1-js/02-first-steps/13-switch/article.md b/1-js/02-first-steps/13-switch/article.md new file mode 100644 index 00000000..4ace9db8 --- /dev/null +++ b/1-js/02-first-steps/13-switch/article.md @@ -0,0 +1,173 @@ +# The "switch" statement + +A `switch` statement can replace multiple `if` checks. + +It gives a more descriptive way to compare a value with multiple variants. + +[cut] + +## The syntax + +The `switch` has one or more `case` blocks and an optional default. + +It looks like this: + +```js no-beautify +switch(x) { + case 'value1': // if (x === 'value1') + ... + [break] + + case 'value2': // if (x === 'value2') + ... + [break] + + default: + ... + [break] +} +``` + +- The value of `x` is checked for a strict equality to the value from the first `case`, that is: `value1`, then to the second `value2` and so on. +- If the equality is found -- `switch` starts to execute the code starting from the corresponding `case`, and to the nearest `break` (or to the end of `switch`). +- If no case matched then the `default` code is executed (if exists). + +## An example + +An example of `switch` (the executed code is highlighted): + +```js run +let a = 2 + 2; + +switch (a) { + case 3: + alert( 'Too small' ); + break; +*!* + case 4: + alert( 'Exactly!' ); + break; +*/!* + case 5: + alert( 'Too large' ); + break; + default: + alert( "I don't know such values" ); +} +``` + +Here the `switch` starts to compare `a` from the first `case` variant that is `3`. The match fails. + +Then `4`. That's the match, so the execution starts from `case 4` and till the nearest `break`. + +**If there is no `break` then the execution continues with the next `case` without any checks.** + +An example without `break`: + +```js run +let a = 2 + 2; + +switch (a) { + case 3: + alert( 'Too small' ); +*!* + case 4: + alert( 'Exactly!' ); + case 5: + alert( 'Too big' ); + default: + alert( "I don't know such values" ); +*/!* +} +``` + +In the example above we'll see sequential execution of three `alert`s: + +```js +alert( 'Exactly!' ); +alert( 'Too big' ); +alert( "I don't know such values" ); +``` + +````smart header="Any expresion can be a `switch/case` argument" +Both `switch` and case allow arbitrary expressions. + +For example: + +```js run +let a = "1"; +let b = 0; + +switch (+a) { +*!* + case b + 1: + alert("this runs, because +a is 1, exactly equals b+1"); + break; +*/!* + + default: + alert("this doesn't run"); +} +``` +Here `+a` gives `1`, that's compared with `b + 1` in `case`, and the corresponding code is executed. +```` + +## Grouping of "case" + +Several variants of `case` which share the same code can be grouped. + +For example, if we want the same code to run for `case 3` and `case 5`: + +```js run no-beautify +let a = 2 + 2; + +switch (a) { + case 4: + alert('Right!'); + break; + +*!* + case 3: // (*) grouped two cases + case 5: + alert('Wrong!'); + alert("Why don't you take a math class?"); + break; +*/!* + + default: + alert('The result is strange. Really.'); +} +``` + +Now both `3` and `5` show the same message. + +The ability to "group" cases a side-effect of how `switch/case` works without `break`. Here the execution of `case 3` starts from the line `(*)` and goes through `case 5`, because there's no `break`. + +## Type matters + +Let's emphasize that the equality check is always strict. The values must be of the same type to match. + +For example, let's consider the code: + +```js run +let arg = prompt("Enter a value?") +switch (arg) { + case '0': + case '1': + alert( 'One or zero' ); + + case '2': + alert( 'Two' ); + break; + + case 3: + alert( 'Never executes!' ); + + default: + alert( 'An unknown value' ) +} +``` + +1. For `0`, `1`, the first `alert` runs. +2. For `2` the second `alert` runs. +3. But for `3`, the result of the `prompt` is a string `"3"`, which is not strictly equal `===` to the number `3`. So we've got a dead code in `case 3`! The `default` variant will execute. diff --git a/1-js/02-first-steps/14-function-basics/1-if-else-required/solution.md b/1-js/02-first-steps/14-function-basics/1-if-else-required/solution.md new file mode 100644 index 00000000..e41c8041 --- /dev/null +++ b/1-js/02-first-steps/14-function-basics/1-if-else-required/solution.md @@ -0,0 +1 @@ +No difference. \ No newline at end of file diff --git a/1-js/02-first-steps/14-function-basics/1-if-else-required/task.md b/1-js/02-first-steps/14-function-basics/1-if-else-required/task.md new file mode 100644 index 00000000..743ba78a --- /dev/null +++ b/1-js/02-first-steps/14-function-basics/1-if-else-required/task.md @@ -0,0 +1,38 @@ +importance: 4 + +--- + +# Is "else" required? + +The following function returns `true` if the parameter `age` is greater than `18`. + +Otherwise it asks for a confirmation and returns its result: + +```js +function checkAge(age) { + if (age > 18) { + return true; +*!* + } else { + // ... + return confirm('Did parents allow you?'); + } +*/!* +} +``` + +Will the function work differently if `else` is removed? + +```js +function checkAge(age) { + if (age > 18) { + return true; + } +*!* + // ... + return confirm('Did parents allow you?'); +*/!* +} +``` + +Is there any difference in the bahavior of these two variants? diff --git a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/solution.md b/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/solution.md new file mode 100644 index 00000000..c8ee9618 --- /dev/null +++ b/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/solution.md @@ -0,0 +1,17 @@ +Using a question mark operator `'?'`: + +```js +function checkAge(age) { + return (age > 18) ? true : confirm('Did parents allow you?'); +} +``` + +Using OR `||` (the shortest variant): + +```js +function checkAge(age) { + return (age > 18) || confirm('Did parents allow you?'); +} +``` + +Note that the parentheses around `age > 18` are not required here. They exist for better readabilty. diff --git a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md b/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md new file mode 100644 index 00000000..f70ad6fa --- /dev/null +++ b/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md @@ -0,0 +1,26 @@ +importance: 4 + +--- + +# Rewrite the function using '?' or '||' + +The following function returns `true` if the parameter `age` is greater than `18`. + +Otherwise it asks for a confirmation and returns its result. + +```js +function checkAge(age) { + if (age > 18) { + return true; + } else { + return confirm('Do you have your parents permission to access this page?'); + } +} +``` + +Rewrite it, to perform the same, but without `if`, in a single line. + +Make two variants of `checkAge`: + +1. Using a question mark operator `'?'` +2. Using OR `||` diff --git a/1-js/02-first-steps/14-function-basics/3-min/solution.md b/1-js/02-first-steps/14-function-basics/3-min/solution.md new file mode 100644 index 00000000..2236d920 --- /dev/null +++ b/1-js/02-first-steps/14-function-basics/3-min/solution.md @@ -0,0 +1,21 @@ +A solution using `if`: + +```js +function min(a, b) { + if (a < b) { + return a; + } else { + return b; + } +} +``` + +A solution with a question mark operator `'?'`: + +```js +function min(a, b) { + return a < b ? a : b; +} +``` + +P.S. In the case of an equality `a == b` it does not matter what to return. \ No newline at end of file diff --git a/1-js/02-first-steps/14-function-basics/3-min/task.md b/1-js/02-first-steps/14-function-basics/3-min/task.md new file mode 100644 index 00000000..50edd0d3 --- /dev/null +++ b/1-js/02-first-steps/14-function-basics/3-min/task.md @@ -0,0 +1,16 @@ +importance: 1 + +--- + +# Function min(a, b) + +Write a function `min(a,b)` which returns the least of two numbers `a` and `b`. + +For instance: + +```js +min(2, 5) == 2 +min(3, -1) == -1 +min(1, 1) == 1 +``` + diff --git a/1-js/02-first-steps/14-function-basics/4-pow/solution.md b/1-js/02-first-steps/14-function-basics/4-pow/solution.md new file mode 100644 index 00000000..79eb2b44 --- /dev/null +++ b/1-js/02-first-steps/14-function-basics/4-pow/solution.md @@ -0,0 +1,23 @@ + +```js run demo +function pow(x, n) { + let result = x; + + for (let i = 1; i < n; i++) { + result *= x; + } + + return result; +} + +let x = prompt("x?", ''); +let n = prompt("n?", ''); + +if (n <= 1) { + alert(`Power ${n} is not supported, + use an integer greater than 0`); +} else { + alert( pow(x, n) ); +} +``` + diff --git a/1-js/02-first-steps/14-function-basics/4-pow/task.md b/1-js/02-first-steps/14-function-basics/4-pow/task.md new file mode 100644 index 00000000..e4f885a5 --- /dev/null +++ b/1-js/02-first-steps/14-function-basics/4-pow/task.md @@ -0,0 +1,19 @@ +importance: 4 + +--- + +# Function pow(x,n) + +Write a function `pow(x,n)` that returns `x` in power `n`. Or, in other words, multiplies `x` by itself `n` times and returns the result. + +```js +pow(3, 2) = 3 * 3 = 9 +pow(3, 3) = 3 * 3 * 3 = 27 +pow(1, 100) = 1 * 1 * ...*1 = 1 +``` + +Create a web-page that prompts for `x` and `n`, and then shows the result of `pow(x,n)`. + +[demo] + +P.S. In this task the function should support only natural values of `n`: integers up from `1`. diff --git a/1-js/02-first-steps/14-function-basics/article.md b/1-js/02-first-steps/14-function-basics/article.md new file mode 100644 index 00000000..0d0dbe92 --- /dev/null +++ b/1-js/02-first-steps/14-function-basics/article.md @@ -0,0 +1,457 @@ +# Functions + +Quite often we need to perform a similar action in many places of the script. + +For example, we need to show a nice-looking message when a visitor logs in, logs out and maybe somewhere else. + +Functions are the main "building blocks" of the program. They allow the code to be called many times without repetition. + +[cut] + +We've already seen examples of built-in functions, like `alert(message)`, `prompt(message, default)` and `confirm(question)`. But we can create functions of our own as well. + +## Function Declaration + +To create a function we can use a *function declaration*. + +It looks like this: + +```js +function showMessage() { + alert( 'Hello everyone!' ); +} +``` + +The `function` keyword goes first, then goes the *name of the function*, then a list of *parameters* in the brackets (empty in the example above) and finally the code of the function, also named "the function body". + +![](function_basics.png) + +Our new function can be called by its name: `showMessage()`. + +For instance: + +```js run +function showMessage() { + alert( 'Hello everyone!' ); +} + +*!* +showMessage(); +showMessage(); +*/!* +``` + +The call `showMessage()` executes the code of the function. Here we will see the message two times. + +This example clearly demonstrates one of the main purposes of functions: to evade code duplication. + +If we ever need to change the message or the way it is shown -- it's enough to modify the code in one place: the function which outputs it. + +## Local variables + +A variable declared inside a function is only visible inside that function. + +For example: + +```js run +function showMessage() { +*!* + let message = "Hello, I'm JavaScript!"; // local variable +*/!* + + alert( message ); +} + +showMessage(); // Hello, I'm JavaScript! + +alert( message ); // <-- Error! The variable is local to the function +``` + +## Outer variables + +A function can access an outer variable as well, for example: + +```js run no-beautify +let *!*userName*/!* = 'John'; + +function showMessage() { + let message = 'Hello, ' + *!*userName*/!*; + alert(message); +} + +showMessage(); // Hello, my name is John +``` + +The function has full access to the outer variable. It can modify it as well. + +For instance: + +```js run +let *!*userName*/!* = 'John'; + +function showMessage() { + *!*userName*/!* = "Bob"; // (1) changed the outer variable + + let message = 'Hello, ' + *!*userName*/!*; + alert(message); +} + +alert( userName ); // *!*John*/!* before the function call + +showMessage(); + +alert( userName ); // *!*Bob*/!*, the value was modified by the function +``` + +The outer variable is only used if there's no local one. So an occasional modification may happen if we forget `let`. + +If a same-named variable is declared inside the function then it *shadows* the outer one. For instance, in the code below the function uses the local `userName`, the outer one is ignored: + +```js run +let userName = 'John'; + +function showMessage() { +*!* + let userName = "Bob"; // declare a local variable +*/!* + + let message = 'Hello, ' + userName; // *!*Bob*/!* + alert(message); +} + +// the function will create and use it's own userName +showMessage(); + +alert( userName ); // *!*John*/!*, unchanged, the function did not access the outer variable +``` + +```smart header="Global variables" +Variables declared outside of any function, such as the outer `userName` in the code above, are called *global*. + +Global variables are visible from any function (unless shadowed by locals). + +Usually, a function declares all variables specific to its task, and global variables only store project-level data, so important that it really must be seen from anywhere. Modern code has few or no globals, most variables reside in their functions. +``` + +## Parameters + +We can pass arbitrary data to function using parameters (also called *function arguments*) . + +In the example below, the function has two parameters: `from` and `text`. + +```js run +function showMessage(*!*from, text*/!*) { // arguments: from, text + alert(from + ': ' + text); +} + +*!* +showMessage('Ann', 'Hello!'); // Ann: Hello! (*) +showMessage('Ann', "What's up?"); // Ann: What's up? (**) +*/!* +``` + +When the function is called in lines `(*)` and `(**)`, the given values are copied to local variables `from` and `next`. Then the function uses them. + +Here's one more example: we have a variable `from` and pass it to the function. Please note: the function changes `from`, but the change is not seen outside, because a function always gets a copy of the value: + + +```js run +function showMessage(from, text) { + +*!* + from = '*' + from + '*'; // make "from" look nicer +*/!* + + alert( from + ': ' + text ); +} + +let from = "Ann"; + +showMessage(from, "Hello"); // *Ann*: Hello + +// the value of "from" is the same, the function modified a local copy +alert( from ); // Ann +``` + +## Default values + +If a parameter is not provided, then its value becomes `undefined`. + +For instance, the aforementioned function `showMessage(from, text)` can be called with a single argument: + +```js +showMessage("Ann"); +``` + +That's not an error. Such call would output `"Ann: undefined"`. There's no `text`, so it's assumed that `text === undefined`. + +If we want to use a "default" `text` in this case, then we can specify it after `=`: + +```js run +function showMessage(from, *!*text = "no text given"*/!*) { + alert( from + ": " + text ); +} + +showMessage("Ann"); // Ann: no text given +``` + +Now if the `text` parameter is not passed, it will get the value `"no text given"` + +Here `"no text given"` is a string, but it can be a more complex expression, which is only evaluated and assigned if the parameter is missing. So, this is also possible: + +```js run +function showMessage(from, text = anotherFunction()) { + // anotherFunction() only executed if no text given + // its result becomes the value of text +} +``` + + +````smart header="Default parameters old-style" +Old editions of JavaScript did not support default parameters. So there are alternative ways to support them, that you can find mostly in the old scripts. + +For instance, an explicit check for being `undefined`: + +```js +function showMessage(from, text) { +*!* + if (text === undefined) { + text = 'no text given'; + } +*/!* + + alert( from + ": " + text ); +} +``` + +...Or the `||` operator: + +```js +function showMessage(from, text) { + // if text is falsy then text gets the "default" value + text = text || 'no text given'; + ... +} +``` + + +```` + + +## Returning a value + +A function can return a value back into the calling code as the result. + +The simplest example would be a function that sums two values: + +```js run no-beautify +function sum(a, b) { + *!*return*/!* a + b; +} + +let result = sum(1, 2); +alert( result ); // 3 +``` + +The directive `return` can be in any place of the function. When the execution reaches it, the function stops, and the value is returned to the calling code (assigned to `result` above). + +There may be many occurences of `return` in a single function. For instance: + +```js run +function checkAge(age) { + if (age > 18) { +*!* + return true; +*/!* + } else { +*!* + return confirm('Got a permission from the parents?'); +*/!* + } +} + +let age = prompt('How old are you?', 18); + +if ( checkAge(age) ) { + alert( 'Access granted' ); +} else { + alert( 'Access denied' ); +} +``` + +It is possible to use `return` without a value. That causes the function to exit immediately. + +For example: + +```js +function showMovie(age) { + if ( !checkAge(age) ) { +*!* + return; +*/!* + } + + alert( "Showing you the movie" ); // (*) + // ... +} +``` + +In the code above, if `checkAge(age)` returns `false`, then `showMovie` won't proceed to the `alert`. + +````smart header="A function with an empty `return` or without it returns `undefined`" +If a function does not return a value, it is the same as if it returns `undefined`: + +```js run +function doNothing() { /* empty */ } + +alert( doNothing() === undefined ); // true +``` + +An empty `return` is also the same as `return undefined`: + +```js run +function doNothing() { + return; +} + +alert( doNothing() === undefined ); // true +``` +```` + +````warn header="Never add a newline between `return` and the value" +For a long expression in `return`, it might be tempting to put it on a separate line, like this: + +```js +return + (some + long + expression + or + whatever * f(a) + f(b)) +``` +That doesn't work, because JavaScript assumes a semicolon after `return`. That'll work the same as: + +```js +return*!*;*/!* + (some + long + expression + or + whatever * f(a) + f(b)) +``` +So, it effectively becomes an empty return. We should put the value on the same line instead. +```` + +## Naming a function [#function-naming] + +Functions are actions. So their name is usually a verb. It should briefly, but as accurately as possible describe what the function does. So that a person who reads the code gets the right clue. + +It is a widespread practice to start a function with a verbal prefix which vaguely describes the action. There must be an agreement within the team on the meaning of the prefixes. + +For instance, functions that start with `"show"` -- usually show something. + +Function starting with... + +- `"get…"` -- return a value, +- `"calc…"` -- calculate something, +- `"create…"` -- create something, +- `"check…"` -- check something and return a boolean, etc. + +Examples of such names: + +```js no-beautify +showMessage(..) // shows a message +getAge(..) // returns the age (gets it somehow) +calcSum(..) // calculates a sum and returns the result +createForm(..) // creates a form (and usually returns it) +checkPermission(..) // checks a permission, returns true/false +``` + +With prefixes at place, a glance at a function name gives an understanding what kind of work it does and what kind of value it returns. + +```smart header="One function -- one action" +A function should do exactly what is suggested by its name, no more. + +Two independant actions usually deserve two functions, even if they are usually called together (in that case we can make a 3rd function that calls those two). + +Few examples of breaking this rule: + +- `getAge` -- would be bad if it shows an `alert` with the age (should only get). +- `createForm` -- would be bad if it modifies the document, adding a form to it (should only create it and return). +- `checkPermission` -- would be bad if displays the `access granted/denied` message (should only perform the check and return the result). + +These examples assume common meanings of prefixes. What they mean for you is determined by you and your team. Maybe it's pretty normal for your code to behave differently. But you should have a firm understanding of what a prefix means, what a prefixed function can and what it cannot do. All same-prefixed functions should obey the rules. And the team should share the knowledge. +``` + +```smart header="Ultrashort function names" +Functions that are used *very often* sometimes have ultrashort names. + +For example, [jQuery](http://jquery.com) framework defines a function `$`, [LoDash](http://lodash.com/) library has it's core function named `_`. + +These are exceptions. Generally functions names should be concise, but descriptive. +``` + +## Functions == Comments + +Functions should be short and do exactly one thing. If that thing is big, maybe it's worth to split the function into few smaller functions. Sometimes following this rule may be not easy, but it's a definitely good thing. + +A separate function is not only easier to test and debug -- its very existence is a great comment! + +For instance, compare the two functions `showPrimes(n)` below. Each one outputs [prime numbers](https://en.wikipedia.org/wiki/Prime_number) up to `n`. + +The first variant uses a label: + +```js +function showPrimes(n) { + nextPrime: for (let i = 2; i < n; i++) { + + for (let j = 2; j < i; j++) { + if (i % j == 0) continue nextPrime; + } + + alert( i ); // a prime + } +} +``` + +The second variant uses an additional function `isPrime(n)` to test for primality: + +```js +function showPrimes(n) { + + for (let i = 2; i < n; i++) { + *!*if (!isPrime(i)) continue;*/!* + + alert(i); // a prime + } +} + +function isPrime(n) { + for (let i = 2; i < n; i++) { + if ( n % i == 0) return false; + } + return true; +} +``` + +The second variant is easier to understand isn't it? Instead of the code piece we see a name of the action (`isPrime`). Sometimes people refer to such code as *self-describing*. + +So, functions can be created even if we don't intend to reuse them. They structure the code and make it readable. + +## Summary + +A function declaration looks like this: + +```js +function name(parameters, delimited, by, comma) { + /* code */ +} +``` + +- Values passed to function as parameters are copied to its local variables. +- A function may access outer variables. But it works only from inside out. The code outside of the function doesn't see its local variables. +- A function can return a value. If it doesn't then its result is `undefined`. + +To make the code clean and easy to understand, it's recommended to use mainly local variables and parameters in the function, not outer variables. + +It is always easier to understand a function which gets parameters, works with them and returns a result than a function which gets no parameters, but modifies outer variables as a side-effect. + +Function naming: + +- A name should clearly describe what the function does. When we see a function call in the code, a good name instantly gives us an understanding what it does and returns. +- A function is an action, so function names are usually verbal. +- There exist many well-known function prefixes like `create…`, `show…`, `get…`, `check…` and so on. Use them to hint what a function does. + +Functions are the main building blocks of scripts. Now we covered the basics, so we actually can start creating and using them. But that's only the beginning of the path. We are going to return to them many times, going more deeply in their advanced features. diff --git a/1-js/02-first-steps/14-function-basics/function_basics.png b/1-js/02-first-steps/14-function-basics/function_basics.png new file mode 100644 index 00000000..7eb6e11e Binary files /dev/null and b/1-js/02-first-steps/14-function-basics/function_basics.png differ diff --git a/1-js/02-first-steps/14-function-basics/function_basics@2x.png b/1-js/02-first-steps/14-function-basics/function_basics@2x.png new file mode 100644 index 00000000..e629bd71 Binary files /dev/null and b/1-js/02-first-steps/14-function-basics/function_basics@2x.png differ diff --git a/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/solution.md b/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/solution.md new file mode 100644 index 00000000..3ea11247 --- /dev/null +++ b/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/solution.md @@ -0,0 +1,17 @@ + +```js run +function ask(question, yes, no) { + if (confirm(question)) yes() + else no(); +} + +ask( + "Do you agree?", +*!* + () => alert("You agreed."), + () => alert("You canceled the execution.") +*/!* +); +``` + +Looks short and clean, right? diff --git a/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/task.md b/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/task.md new file mode 100644 index 00000000..a888ac15 --- /dev/null +++ b/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/task.md @@ -0,0 +1,17 @@ + +# Rewrite with arrow functions + +Replace Function Expressions with arrow functions in the code: + +```js run +function ask(question, yes, no) { + if (confirm(question)) yes() + else no(); +} + +ask( + "Do you agree?", + function() { alert("You agreed."); }, + function() { alert("You canceled the execution."); } +); +``` diff --git a/1-js/02-first-steps/15-function-expressions-arrows/article.md b/1-js/02-first-steps/15-function-expressions-arrows/article.md new file mode 100644 index 00000000..44cf3764 --- /dev/null +++ b/1-js/02-first-steps/15-function-expressions-arrows/article.md @@ -0,0 +1,482 @@ +# Function expressions and arrows + +In JavaScript a function is not a "magical language structure", but a special kind of value. + +[cut] + +The syntax that we used before is called *Function Declaration*: + +```js +function sayHi() { + alert( "Hello" ); +} +``` + +There is another syntax of creating a function that is called *Function Expression*. + +It looks like this: + +```js +let sayHi = function() { + alert( "Hello" ); +}; +``` + +Here the function is created and assigned to the variable explicitly, like any other value. No matter, how the function is defined -- it's just a value, stored in the variable `sayHi`. + + +The meaning of these code samples is the same: "create a function and put it into the variable `sayHi`". + +We can even print out that value using `alert`: + +```js run +function sayHi() { + alert( "Hello" ); +} + +*!* +alert( sayHi ); // shows the function code +*/!* +``` + +Please note that the last line does not run the function, because there are no parentheses after `sayHi`. There are programming languages where any mention of a function name causes the execution, but JavaScript is not like that. + +In JavaScript, a function is a value and we can deal with that as a value. The code above shows its string representation, that is the source code. + +It is a special value of course, in the sense that we can call it like `sayHi()`. + +But it's still a value. So we can work with it like with other kinds of values. + +We can copy a function to another variable: + +```js run no-beautify +function sayHi() { // (1) create + alert( "Hello" ); +} + +let func = sayHi; // (2) copy + +func(); // Hello // (3) run the copy (it works)! +sayHi(); // Hello // this still works too (why wouldn't it) +``` + +That's what happens above in detail: + +1. Function Declaration `(1)` creates the function and puts it into the variable named `sayHi`. +2. Line `(2)` copies it into variable `func`. + + Please note again: there are no parentheses after `sayHi`. If they were, then `func = sayHi()` would write *the result of the call* `sayHi()` into `func`, not *the function* `sayHi` itself. +3. Now the function can be called both as `sayHi()` and `func()`. + +Note, that we could also have used a Function Expression to declare `sayHi`, in the first line: + +```js +let sayHi = function() { ... }; + +let func = sayHi; +// ... +``` + +Everything would work the same. Even more obvious what's going on, right? + + +````smart header="Why there's a semicolon at the end?" +There might be a question, why Function Expression has a semicolon `;` at the end, and Function Declaration does not: + +```js +function sayHi() { + // ... +} + +let sayHi = function() { + // ... +}*!*;*/!* +``` + +The answer is simple: +- There's no need in `;` at the end of code blocks and syntax structures that use them like `if { ... }`, `for { }`, `function f { }` etc. +- A Function Expression is used inside the statement: `let sayHi = ...;`, as a value. It's not a code block. The semicolon `;` is recommended at the end of statements, no matter what is the value. So the semicolon here is not related to Function Expression itself in any way, it just terminates the statement. +```` + +## Callback functions + +Let's see more examples of passing functions as values and using function expressions. + +We'll write a function `ask(question, yes, no)` with three parameters: + +`question` +: Text of the question + +`yes` +: Function to run if the answer is "Yes" + +`no` +: Function to run if the answer is "No" + +The function should ask the `question` and, depending on the user's answer, call `yes()` or `no()`: + +```js run +*!* +function ask(question, yes, no) { + if (confirm(question)) yes() + else no(); +} +*/!* + +function showOk() { + alert( "You agreed." ); +} + +function showCancel() { + alert( "You canceled the execution." ); +} + +// usage: functions showOk, showCancel are passed as arguments to ask +ask("Do you agree?", showOk, showCancel); +``` + +Before we'll explore how we can write it in a much shorter way, let's note that in the browser (and on the server-side in some cases) such functions are quite popular. + +The major difference between a real-life implementation and the example above is that real-life functions use more complex ways to interact with the user than a simple `confirm`. In the browser such a function usually draws a nice-looking question window. But that's another story. + +**The arguments of `ask` are called *callback functions* or just *callbacks*. The idea is that we pass a function and expect it to be "called back" in certain circumstances.** + +So, `showOk` becomes the callback for the "yes" answer and `showCancel` -- for the "no" answer. + +We can use Function Expressions to write the same much shorter: + +```js run no-beautify +function ask(question, yes, no) { + if (confirm(question)) yes() + else no(); +} + +*!* +ask( + "Do you agree?", + function() { alert("You agreed."); }, + function() { alert("You canceled the execution."); } +); +*/!* +``` + +Here functions are declared right inside the `ask(...)` call. They have no name, and so are called *anonymous*. Such functions are not accessible outside of `ask` (because they are not assigned to variables), but that's just what we want here. + +Such code appears in our scripts very naturally, it's in the spirit of JavaScript. + + +```smart header="A function is a value representing an \"action\"" +Regular values like strings or numbers represent the *data*. + +A function can be perceived as an *action*. + +We can pass it between variables and run when we want. +``` + + +## Function Expression vs Function Declaration + +Let's formulate the key differences between Function Declarations and Expressions. + +First, the syntax: how to see what is what in the code. + +- *Function Declaration:* a function, declared as a separate statement, in the main code flow. + + ```js + // Function Declaration + function sum(a, b) { + return a + b; + } + ``` +- *Function Expression:* a function, created inside an expression or inside another syntax construct. + + Here the function is created at the right side of the "assignment expression =": + ```js + // Function Expression + let sum = function(a, b) { + return a + b; + } + ``` + +The more subtle difference is *when* a function is created by JavaScript engine. + +**Function Expression is created when the execution reaches it and is usable since then.** + +Once the execution flow passes to the right side of the assignment `let sum = function…` -- here we go, the function is created and can be used (assigned, called etc) from now on. + +Function Declarations are different. + +**Function Declaration is usable in the whole script/code block.** + +In other words, when JavaScript *prepares* to run the script or a code block, it first looks for Function Declarations in it and creates the functions. We can think of it as an "initialization stage". + +And after all Function Declarations are processed, the execution goes on. + +As a natural effect, a function declared as Function Declaration can be called earlier than it is defined. + +For example, this works: + +```js run refresh untrusted +*!* +sayHi("John"); // Hello, John +*/!* + +function sayHi(name) { + alert( `Hello, ${name}` ); +} +``` + +Function Declaration `sayHi` is created when JavaScript is preparing to start the script and is visible everywhere in it. + +...And if there were Function Expression, then it wouldn't work: + +```js run refresh untrusted +*!* +sayHi("John"); // error! +*/!* + +let sayHi = function(name) { // (*) no magic any more + alert( `Hello, ${name}` ); +}; +``` + +Function Expressions are created when the execution reaches them. That would happen only in the line `(*)`. Too late. + + +### Function Declaration in a block + +When Function Declaration is made within a code block, it is visible everywhere inside that block. But not outside of it. + +Sometimes that's handy to declare a local function only needed in that only block. But that feature may also cause problems. + +For instance, let's imagine that we need to declare a function `welcome()` depending on the `age` variable that we get in run time. And then we plan to use it sometimes later. + +The code below doesn't work: + +```js run +let age = prompt("What is your age?", 18); + +if (age < 18) { + + function welcome() { + alert("Hello!"); + } + +} else { + + function welcome() { + alert("Greetings!"); + } + +} + +*!* +welcome(); // Error: welcome is not defined +*/!* +``` + +The Function Declaration is visible only inside the code block where it resides. + +We can call it from within the block, but not from outside: + +```js run +let age = 16; // take 16 as an example + +if (age < 18) { +*!* + welcome(); // \ (runs) +*/!* + // | + function welcome() { // | + alert("Hello!"); // | Function Declaration is available + } // | everywhere in the block where it's declared + // | +*!* + welcome(); // / (runs) +*/!* + +} else { + // \ + function welcome() { // | + alert("Greetings!"); // | if age=16, the the execution does not go here, + } // | so this "welcome" is never created + // / +} + +// Here we're out of figure brackets, +// so we can not see Function Declarations made inside of them. + +*!* +welcome(); // Error: welcome is not defined +*/!* +``` + +What can we do to make `welcome` visible outside of `if`? + +The right thing would be to use a Function Expression and assign `welcome` to the variable which is declared outside of `if` and has the proper visibility: + +```js run +let age = prompt("What is your age?", 18); + +let welcome; + +if (age < 18) { + + welcome = function() { + alert("Hello!"); + }; + +} else { + + welcome = function() { + alert("Greetings!"); + }; + +} + +*!* +welcome(); // ok now +*/!* +``` + +Or we could simplify it even further using a question mark operator `?`: + +```js run +let age = prompt("What is your age?", 18); + +let welcome = (age < 18) ? + function() { alert("Hello!"); } : + function() { alert("Greetings!"); }; + +*!* +welcome(); // ok now +*/!* +``` + + +```smart header="What to choose: Function Declaration or Function Expression?" +As a rule of thumb, when we need to declare a function, the first to consider is Function Declaration syntax, the one we used before. It gives more freedom in how to organize our code, because we can call such functions before they are declared. + +It's also a little bit easier to look up `function f(…) {…}` in the code than `let f = function(…) {…}`. Function Declarations are more "eye-catching". + +...But if Function Declaration does not suit us for some reason (we've seen an example above), then Function Expression should be used. +``` + + +## Arrow functions [#arrow-functions] + +There's one more syntax for creating functions -- very simple and concise. It's called "arrow functions", because it looks like this: + + +```js +let func = (arg1, arg2, ...argN) => expression +``` + +...This creates a function `func` that has arguments `arg1..argN`, evaluates the `expression` on the right side with their use and returns its result. + +In other words, it's roughly the same as: + +```js +let func = function(arg1, arg2, ...argN) { + return expression; +} +``` + +...But much shorter. + +Let's see the example: + +```js run +let sum = (a, b) => a + b; + +alert( sum(1, 2) ); // 3 +``` + +Here the arrow function is a shorter form of: + +```js +let sum = function(a, b) { + return a + b; +}; +``` + +If we have only one argument, then parentheses can be omitted, making that even shorter: + +```js run +// same as +// let double = function(n) { return n*2 } +*!* +let double = n => n*2; +*/!* + +alert( double(3) ); // 6 +``` + +If there are no arguments, we can put empty parentheses: + +```js run +let sayHi = () => alert("Hello!"); + +sayHi(); +``` + +Arrow functions can be used same way as Function Expressions. + +For instance, here's the rewritten example with `welcome()`: + +```js run +let age = prompt("What is your age?", 18); + +let welcome = (age < 18) ? + () => alert('Hello') : + () => alert("Greetings!"); + +welcome(); // ok now +``` + +The syntax may appear unfamiliar and not very readable at first, but that quickly changes as the eyes get used to the structure. + +Arrow functions are very convenient for simple one-line actions, when we're just lazy to write many words. + +```smart header="Multiline arrow functions" + +The examples above took arguments from the left of `=>` and evaluate the right-side expression with them. + +Sometimes we need something a little bit more complex, like multiple expressions or statements. It is also possible, but we should enclose them in figure brackets. Then use a normal `return` within them. + +Like this: + +```js run +let sum = (a, b) => { // the figure bracket opens a multiline function + let result = a + b; +*!* + return result; // if we use figure brackets, must use return +*/!* +}; + +alert( sum(1, 2) ); // 3 +``` + +```smart header="More to come" +Here we praised arrow functions for brevity. But that's not all! Arrow functions have other interesting features. We'll return to them later in the chapter . + +As for now, we can already use them for one-line actions and callbacks. +``` + +## Summary + +- Functions are values. They can be assigned, copied or declared in any place of the code. +- If the function is declared as a separate statement, in the main code flow -- that's called "Function Declaration". +- If the function is created as a part of an expression -- it's "Function Expression". +- Function Declarations are processed before the code block is executed. They are visible everywhere in the block. +- Function Expressions are created when the execution flow reaches them. + + +In most cases when we need to declare a function, Function Declaration is preferable, because it is visible prior to the declaration itself. That gives more flexibility in code organization. And is usually more readable. + +So we should use Function Expression only when Function Declaration does not fit the task. We've seen a couple of examples of that in the chapter. And will see more in the future. + +Arrow functions are handy for one-liners. The come in two flavors: + +1. Without figure brackets: `(...args) => expression` -- the right side is an expression: the function evaluates it and returns the result. +2. With figure brackets: `(...args) => { body }` -- brackets allow to write multiple statements inside the function, but we need an explicit `return` to return something. diff --git a/1-js/02-first-steps/16-javascript-specials/article.md b/1-js/02-first-steps/16-javascript-specials/article.md new file mode 100644 index 00000000..2f71e987 --- /dev/null +++ b/1-js/02-first-steps/16-javascript-specials/article.md @@ -0,0 +1,289 @@ +# JavaScript specials + +This chapter briefly recaps the features of JavaScript that we've learned by now, paying special attention to subtle moments. + +[cut] + +## Code structure + +Statements are delimited with a semicolon: + +```js run no-beautify +alert('Hello'); alert('World'); +``` + +Usually, a line-break is also treated as a delimiter, so that would also work: + +```js run no-beautify +alert('Hello') +alert('World') +``` + +That's called "automatic semicolon insertion". Sometimes it doesn't work, for instance: + +```js run +alert("There will be an error after this message") + +[1, 2].forEach(alert) +``` + +Most codestyle guides agree that we should put a semicolon after each statement. + +Semicolons are not required after code blocks `{...}` and syntax constructs with them like loops: + +```js +function f() { + // no semicolon needed after function declaration +} + +for(;;) { + // no semicolon needed after the loop +} +``` + +...But even if we can put an "extra" semicolon somewhere, that's not an error, it will be ignored. + +More in: . + +## Strict mode + +To fully enable all features of modern JavaScript, we should start scripts with `"use strict"`. + +```js +'use strict'; + +... +``` + +The directive must be at the top of a script or at the beginning of a function. + +Without `"use strict"`, everything still works, but some features behave in old-fasion, "compatible" way. We'd generally prefer the modern behavior. + +Some modern features of the language (like classes that we'll study in the future) enable strict mode implicitly. + +More in: . + +## Variables + +Can be declared using: + +- `let` +- `const` (constant, can't be changed) +- `var` (old-style, will see later) + +A variable name can include: +- Letters and digits, but the first character may not be a digit. +- Characters `$` and `_` are normal, on par with letters. +- Non-latin alphabets and hieroglyphs are also allowed, but commonly not used. + +Variables are dynamically typed -- they can store any value: + +```js +let x = 5; +x = "John"; +``` + +There are 7 data types: + +- `number` for both floating-point and integer numbers, +- `string` for strings, +- `boolean` for logical values: `true/false`, +- `null` -- a type with a single value `null`, meaning "empty" or "does not exist", +- `undefined` -- a type with a single value `undefined`, meaning "not assigned", +- `object` and `symbol` -- for complex data structures and unique identifiers, we didn't learn them yet. + +The `typeof` operator returns the type for a value, with two exceptions: +```js +typeof null == "object" // error in the language +typeof function(){} == "function" // functions are treated specially +``` + +More in: and . + +## Interaction + +We're using a browser as a working environment, so basic UI functions will be: + +[`prompt(question[, default])`](mdn:api/Window/prompt) +: Ask a `question`, and return either what the visitor entered or `null` if he pressed "cancel". + +[`confirm(question)`](mdn:api/Window/confirm) +: Ask a `question` and suggest to choose between Ok and Cancel. The choice is returned as `true/false`. + +[`alert(message)`](mdn:api/Window/alert) +: Output a `message`. + +All these functions are *modal*, they pause the code execution and prevent the visitor from interaction with the page until he answers. + +For instance: + +```js run +let userName = prompt("Your name?", "Alice"); +let isTeaWanted = confirm("Do you want some tea?"); + +alert( "Visitor: " + userName ); // Alice +alert( "Tea wanted: " + isTeaWanted ); // true +``` + +More in: . + +## Operators + +JavaScript supports following operators: + +Arithmetical +: Regular: `* + - /`, also `%` for the remainder and `**` for power of a number. + + Binary plus `+` concatenates strings. And if any of the operands is a string -- the other one is converted to string too: + + ```js run + alert( '1' + 2 ); // '12', string + alert( 1 + '2' ); // '12', string + ``` + +Assignments +: There is a simple assignment: `a = b` and combined ones like `a *= 2`. + +Bitwise +: Bitwise operators work with integers on bit-level: see the [docs](mdn:/JavaScript/Reference/Operators/Bitwise_Operators) when they are needed. + +Ternary +: The only operator with three parameters: `cond ? resultA : result B`. If `cond` is truthy, returns `resultA`, otherwise `resultB`. + +Logical operators +: Logical AND `&&` and OR `||` perform short-circuit evaluation and then return the value where it stopped. + +Comparisons +: Equality check `==` for values of different types converts them to a number (except `null` and `undefined` that equal each other and nothing else), so these are equal: + + ```js run + alert( 0 == false ); // true + alert( 0 == '' ); // true + ``` + + Other comparisons convert to a number as well. + + The strict equality operator `===` doesn't do the conversion: different types always mean different values for it, so: + + Values `null` and `undefined` are special: they equal `==` each other and don't equal anything else. + + Greater/less comparisons compare strings character-by-character, other types are converted to a number. + +Logical operators +: There are few others, like a comma operator. + +More in: , , . + +## Loops + +- We covered 3 types of loops: + + ```js + // 1 + while (condition) { + ... + } + + // 2 + do { + ... + } while (condition); + + // 3 + for(let i = 0; i < 10; i++) { + ... + } + ``` + +- The variable declared in `for(let...)` loop is visible only inside the loop. But we can also omit `let` and reuse an existing variable. +- Directives `break/continue` allow to exit the whole loop/current iteration. Use labels to break nested loops. + +Details in: . + +Later we'll study more types of loops to deal with objects. + +## The "switch" construct + +The "switch" construct can replace multiple `if` checks. It uses `===` for comparisons. + +For instance: + +```js run +let age = prompt('Your age?', 18); + +switch (age) { + case 18: + alert("Won't work"); // the result of prompt is a string, not a number + + case "18": + alert("This works!""); + break; + + default: + alert("Any value not equal to one above"); +} +``` + +Details in: . + +## Functions + +We covered 3 ways to create a function in JavaScript: + +1. Function Declaration: the function in the main code flow + + ```js + function sum(a, b) { + let result = a + b; + + return result; + } + ``` + +2. Function Expression: the function in the context of an expression + + ```js + let sum = function(a, b) { + let result = a + b; + + return result; + } + ``` + + Function expression can have a name, like `sum = function name(a, b)`, but that `name` is only visible inside that function. + +3. Arrow functions: + + ```js + // expression at the right side + let sum = (a, b) => a + b; + + // or multiline syntax with { ... }, need return here: + let sum = (a, b) => { + // ... + return a + b; + } + + // without arguments + let sayHi = () => alert("Hello"); + + // with a single argument + let double = n => n * 2; + ``` + + +- Functions may have local variables -- those declared inside its body. Such variables are only visible inside the function. +- Parameters can have default values: `function sum(a=1, b=2) {...}`. +- Functions always return something. If there's no `return` statement, then the result is `undefined`. + + +| Function Declaration | Function Expression | +|----------------------|---------------------| +| visible in the whole code block | created when the execution reaches it | +| - | can have a name, visible only inside the function | + +More: see , . + +## More to come + +That was a brief list of JavaScript features. As of now we studied only basics. Further in the tutorial you'll find more specials and advanced features of JavaScript. diff --git a/1-js/02-first-steps/index.md b/1-js/02-first-steps/index.md new file mode 100644 index 00000000..31281656 --- /dev/null +++ b/1-js/02-first-steps/index.md @@ -0,0 +1,3 @@ +# JavaScript Fundamentals + +Let's learn the fundamentals of script building. \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/article.md b/1-js/03-code-quality/01-debugging-chrome/article.md new file mode 100644 index 00000000..44bc1fbe --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/article.md @@ -0,0 +1,185 @@ +# Debugging in Chrome + +Before writing more complex code, let's talk about debugging. + +All modern browsers and most other environments support "debugging" -- a special UI in developer tools that makes finding and fixing errors much easier. + +We'll be using Chrome here, because it's probably the most feature-rich in this aspect. + +[cut] + +## The "sources" pane + +Your Chrome version may look a little bit different, but it still should be obvious what's there. + +- Open the [example page](debugging/index.html) in Chrome. +- Turn on developer tools with `key:F12` (Mac: `key:Cmd+Opt+I`). +- Select the `sources` pane. + +Here's what you should see if you are doing it for the first time: + +![](chrome-open-sources.png) + +The toggler button opens the tab with files. + +Let's click it and select `index.html` and then `hello.js` in the tree view. That's what should show up: + +![](chrome-tabs.png) + +Here we can see three zones: + +1. **Resources zone** lists html, javascript, css and other files including images that are attached to the page. Chrome extensions may appear here too. +2. **Source zone** shows the source code. +3. **Information and control zone** 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. + +## Console + +If we press `Esc`, then a console opens below, we can type commands there and press `key:Enter` to execute. + +After a statement is executed, its result is shown below. + +For example, here `1+2` results in `3`, and `hello("debugger")` returns nothing, so the result is `undefined`: + +![](chrome-sources-console.png) + +## Breakpoints + +Let's examine what's going on within the code of the [example page](debugging/index.html). In `hello.js`, click at the line number `4`. Yes, right on the `"4"` digit, not on the code. + +Contratulations! You've set a breakpoint. Please also click on the number for line `8`. + +Should look like this (blue is where you should click): + +![](chrome-sources-breakpoint.png) + +A *breakpoint* is a point of code where the debugger will automatically pause the JavaScript execution. + +While the code is paused, we can examine current variables, execute commands in the console etc. In other words, to debug it. + +We can always find a list of breakpoints in the right pane. That's useful when we have many breakpoints in various files. It allows to: +- Quickly jump to the breakpoint in the code (by clicking on it in the right pane). +- Temporarily disable the breakpoint by unchecking it. +- Remove the breakpoint by right-clicking and selecting Remove. +- ...And so on. + +```smart header="Conditional breakpoints" +*Right click* on the line number allows to create a *conditional* breakpoint. It only triggers when the given expression is truthy. + +That's handy when we need to stop only for a certain variable value or for a certain function parameters. +``` + +## Debugger command + +We can also pause the code by using the `debugger` command, like this: + +```js +function hello(name) { + let phrase = `Hello, ${name}!`; + +*!* + debugger; // <-- the debugger stops here +*/!* + + say(phrase); +} +``` + +That's very convenient when we are in a code editor and don't want to switch to the browser and look up the script in developer tools to set the breakpoint. + + +## Pause and look around + +In our example, `hello()` is called during the page load, so the easiest way to activate debugger -- is to reload the page. So let's press `key:F5` (Windows, Linux) or `key:Cmd+R` (Mac). + +As the breakpoint is set, the execution pauses at the 4th line: + +![](chrome-sources-debugger-pause.png) + +Please open the informational dropdowns to the right (labelled with arrows). They allow to examine the current code state: + +1. **`Watch` -- shows current values for any expressions.** + + You can click the plus `+` and input an expression. The debugger will shown its value at any moment, automatically recalculating it in in the process of execution. + +2. **`Call Stack` -- shows the nested calls chain.** + + At the current moment the debugger is inside `hello()` call, called by a script in `index.html` (no function there, so it's called "anonymous"). + + If you click on a stack item, the debugger jumps to the corresponding code, and all its variables can be examined as well. +3. **`Scope` -- current variables.** + + `Local` shows local function variables. You can also see their values highlighted right over the source. + + `Global` has global variables (out of any functions). + + There's also `this` keyword there that we didn't study yet, but we'll do that soon. + +## Tracing the execution + +Now it's time to *trace* the script. + +There are buttons for it at the top of the right pane. Let's engage them. + + -- continue the execution, hotkey `key:F8`. +: Resumes the execution. If there are no additional breakpoints, then the execution just continues and the debugger looses the control. + + Here's what we can see after a click on it: + + ![](chrome-sources-debugger-trace-1.png) + + The execution has resumed, reached another breakpoint inside `say()` and paused there. Take a look at the "Call stack" at the right. It has increased by one more call. We're inside `say()` now. + + -- make a step (run the next command), but *not go into the function*, hotkey `key:F10`. +: If we click it now, `alert` will be shown. The important thing is that `alert` can be any function, the execution "steps over it", skipping the function internals. + + -- make a step, hotkey `key:F11`. +: The same as the previous one, but "steps into" nested functions. Clicking this will step through all script actions one by one. + + -- continue the execution till the end of the current function, hotkey `key:Shift+F11`. +: The execution would stop at the very last line of the current function. That's handy when we accidentally entered a nested call using , but it does not interest us, and we want to continue to its end as soon as possible. + + -- enable/disable all breakpoints. +: That button does not move the execution. Just a mass on/off for breakpoints. + + -- enable/disable automatic pause in case of an error. +: When enabled, and the developer tools is open, a script error automatically pauses the execution. Then we can analyze variables to see what went wrong. So if our script dies with an error, we can open debugger, enable this option and reload the page to see where it dies and what's the context at that moment. + +```smart header="Continue to here" +Right click on a line of code opens the context menu with a great option called "Continue to here". + +That's handy when we want to move multiple steps forward, but too lazy to set a breakpoint. +``` + +## Logging + +To output something to console, there's `console.log` function. + +For instance, this outputs values from `0` to `4` to console: + +```js run +// open console to see +for (let i = 0; i < 5; i++) { + console.log("value", i); +} +``` + +Regular users don't see that output, it is in the console. To see it -- either open the Console tab of developer tools or press `key:Esc` while in another tab: that opens the console at the bottom. + +If we have enough logging in our code, then we can see what's going on from the records, without the debugger. + +## Summary + +As we can see, there are 3 main ways to pause a script: +1. A breakpoint. +2. The `debugger` statements. +3. An error (if dev tools are open and the button is "on") + +Then we can examine variables and step on to see where the execution goes the wrong way. + +There's much more options in developer tools than covered here. The full manual is at + +The information from this chapter is enough to begin debugging, but later, especially if you do a lot of browser stuff, please go there and look through more advanced capabilities of developer tools. + +Oh, and also you can click at various places of dev tools and just see what's showing up. That's probably the fastest route to learn dev tools. Don't forget about the right click as well! diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.png b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.png new file mode 100644 index 00000000..7d779337 Binary files /dev/null and b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.png differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources@2x.png new file mode 100644 index 00000000..bdc8d72c Binary files /dev/null and b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources@2x.png differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.png new file mode 100644 index 00000000..2fe449c9 Binary files /dev/null and b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.png differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint@2x.png new file mode 100644 index 00000000..e4abc89d Binary files /dev/null and b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint@2x.png differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.png new file mode 100644 index 00000000..98b22e77 Binary files /dev/null and b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.png differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console@2x.png new file mode 100644 index 00000000..3269a80f Binary files /dev/null and b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console@2x.png differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.png new file mode 100644 index 00000000..719293d2 Binary files /dev/null and b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.png differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause@2x.png new file mode 100644 index 00000000..5c22ab36 Binary files /dev/null and b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause@2x.png differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.png new file mode 100644 index 00000000..1848ccfa Binary files /dev/null and b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.png differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1@2x.png new file mode 100644 index 00000000..fcabf722 Binary files /dev/null and b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1@2x.png differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.png b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.png new file mode 100644 index 00000000..ff91c531 Binary files /dev/null and b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.png differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs@2x.png new file mode 100644 index 00000000..09b10bf4 Binary files /dev/null and b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_break_error.png b/1-js/03-code-quality/01-debugging-chrome/chrome_break_error.png similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/chrome_break_error.png rename to 1-js/03-code-quality/01-debugging-chrome/chrome_break_error.png diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_break_error@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_break_error@2x.png old mode 100755 new mode 100644 similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/chrome_break_error@2x.png rename to 1-js/03-code-quality/01-debugging-chrome/chrome_break_error@2x.png diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources.png similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/chrome_sources.png rename to 1-js/03-code-quality/01-debugging-chrome/chrome_sources.png diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources@2x.png old mode 100755 new mode 100644 similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/chrome_sources@2x.png rename to 1-js/03-code-quality/01-debugging-chrome/chrome_sources@2x.png diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources_break.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break.png similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/chrome_sources_break.png rename to 1-js/03-code-quality/01-debugging-chrome/chrome_sources_break.png diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources_break@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break@2x.png old mode 100755 new mode 100644 similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/chrome_sources_break@2x.png rename to 1-js/03-code-quality/01-debugging-chrome/chrome_sources_break@2x.png diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources_breakpoint.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint.png similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/chrome_sources_breakpoint.png rename to 1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint.png diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources_breakpoint@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint@2x.png old mode 100755 new mode 100644 similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/chrome_sources_breakpoint@2x.png rename to 1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint@2x.png diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources_buttons.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons.png similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/chrome_sources_buttons.png rename to 1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons.png diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources_buttons@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons@2x.png old mode 100755 new mode 100644 similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/chrome_sources_buttons@2x.png rename to 1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons@2x.png diff --git a/1-js/3-writing-js/1-debugging-chrome/console_error.png b/1-js/03-code-quality/01-debugging-chrome/console_error.png similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/console_error.png rename to 1-js/03-code-quality/01-debugging-chrome/console_error.png diff --git a/1-js/3-writing-js/1-debugging-chrome/console_error@2x.png b/1-js/03-code-quality/01-debugging-chrome/console_error@2x.png old mode 100755 new mode 100644 similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/console_error@2x.png rename to 1-js/03-code-quality/01-debugging-chrome/console_error@2x.png diff --git a/1-js/03-code-quality/01-debugging-chrome/debugging.view/hello.js b/1-js/03-code-quality/01-debugging-chrome/debugging.view/hello.js new file mode 100644 index 00000000..4236740e --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/debugging.view/hello.js @@ -0,0 +1,9 @@ +function hello(name) { + let phrase = `Hello, ${name}!`; + + say(phrase); +} + +function say(phrase) { + alert(`** ${phrase} **`); +} diff --git a/1-js/03-code-quality/01-debugging-chrome/debugging.view/index.html b/1-js/03-code-quality/01-debugging-chrome/debugging.view/index.html new file mode 100644 index 00000000..6c651e85 --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/debugging.view/index.html @@ -0,0 +1,14 @@ + + + + + + + An example for debugging. + + + + + diff --git a/1-js/03-code-quality/01-debugging-chrome/head.html b/1-js/03-code-quality/01-debugging-chrome/head.html new file mode 100644 index 00000000..f219b0af --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/head.html @@ -0,0 +1,8 @@ + diff --git a/1-js/3-writing-js/1-debugging-chrome/manage1.png b/1-js/03-code-quality/01-debugging-chrome/manage1.png similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/manage1.png rename to 1-js/03-code-quality/01-debugging-chrome/manage1.png diff --git a/1-js/3-writing-js/1-debugging-chrome/manage1@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage1@2x.png old mode 100755 new mode 100644 similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/manage1@2x.png rename to 1-js/03-code-quality/01-debugging-chrome/manage1@2x.png diff --git a/1-js/3-writing-js/1-debugging-chrome/manage2.png b/1-js/03-code-quality/01-debugging-chrome/manage2.png similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/manage2.png rename to 1-js/03-code-quality/01-debugging-chrome/manage2.png diff --git a/1-js/3-writing-js/1-debugging-chrome/manage2@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage2@2x.png old mode 100755 new mode 100644 similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/manage2@2x.png rename to 1-js/03-code-quality/01-debugging-chrome/manage2@2x.png diff --git a/1-js/3-writing-js/1-debugging-chrome/manage3.png b/1-js/03-code-quality/01-debugging-chrome/manage3.png similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/manage3.png rename to 1-js/03-code-quality/01-debugging-chrome/manage3.png diff --git a/1-js/3-writing-js/1-debugging-chrome/manage3@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage3@2x.png old mode 100755 new mode 100644 similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/manage3@2x.png rename to 1-js/03-code-quality/01-debugging-chrome/manage3@2x.png diff --git a/1-js/3-writing-js/1-debugging-chrome/manage4.png b/1-js/03-code-quality/01-debugging-chrome/manage4.png similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/manage4.png rename to 1-js/03-code-quality/01-debugging-chrome/manage4.png diff --git a/1-js/3-writing-js/1-debugging-chrome/manage4@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage4@2x.png old mode 100755 new mode 100644 similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/manage4@2x.png rename to 1-js/03-code-quality/01-debugging-chrome/manage4@2x.png diff --git a/1-js/3-writing-js/1-debugging-chrome/manage5.png b/1-js/03-code-quality/01-debugging-chrome/manage5.png similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/manage5.png rename to 1-js/03-code-quality/01-debugging-chrome/manage5.png diff --git a/1-js/3-writing-js/1-debugging-chrome/manage5@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage5@2x.png old mode 100755 new mode 100644 similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/manage5@2x.png rename to 1-js/03-code-quality/01-debugging-chrome/manage5@2x.png diff --git a/1-js/3-writing-js/1-debugging-chrome/manage6.png b/1-js/03-code-quality/01-debugging-chrome/manage6.png similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/manage6.png rename to 1-js/03-code-quality/01-debugging-chrome/manage6.png diff --git a/1-js/3-writing-js/1-debugging-chrome/manage6@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage6@2x.png old mode 100755 new mode 100644 similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/manage6@2x.png rename to 1-js/03-code-quality/01-debugging-chrome/manage6@2x.png diff --git a/1-js/03-code-quality/01-debugging-chrome/toolbarButtonGlyphs.svg b/1-js/03-code-quality/01-debugging-chrome/toolbarButtonGlyphs.svg new file mode 100644 index 00000000..5bdf20a8 --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/toolbarButtonGlyphs.svg @@ -0,0 +1,1035 @@ + +image/svg+xml \ No newline at end of file diff --git a/1-js/03-code-quality/02-coding-style/1-style-errors/solution.md b/1-js/03-code-quality/02-coding-style/1-style-errors/solution.md new file mode 100644 index 00000000..810fdad5 --- /dev/null +++ b/1-js/03-code-quality/02-coding-style/1-style-errors/solution.md @@ -0,0 +1,48 @@ + +You could note the following: + +```js no-beautify +function pow(x,n) // <- no space between arguments +{ // <- figure bracket on a separate line + let result=1; // <- no spaces to the both sides of = + for(let i=0;i + +Now let's discuss the rules and reasons for them in detail. + +Nothing is "carved in stone" here. Everything is optional and can be changed: these are coding rules, not religious dogmas. + +### Figure brackets + +In most JavaScript projects figure brackets are written on the same line, not on the new line. A so-called "egyptian" style. There's also a space before an opening bracket. + +An edge case is a single-line `if/for`. Should we use brackets at all? If yes, then where? + +Here are the annotated variants, so you can judge about their readability on your own: + + +![](figure-bracket-style.png) + +As a summary: +- For a really short code one line is acceptable: like `if (cond) return null`. +- But a separate line for each statement in brackets is usually better. + +### Line length + +The maximal line length should be limited. No one likes to eye-follow a long horizontal line. It's better to split it. + +The maximal line length is agreed on the team-level. It's usually 80 or 120 characters. + +### Indents + +There are two types of indents: + +- **A horizontal indent: 2(4) spaces.** + + A horizantal identation is made using either 2 or 4 spaces or the "Tab" symbol. Which one to choose is a kind of an old holy war. Spaces are more common nowadays. + + One of advantages of spaces over tabs is that they allow more flexible configurations of indents than the "Tab" symbol. + + For instance, we can align the arguments with the opening bracket, like this: + + ```js no-beautify + show(parameters, + aligned, // 5 spaces padding at the left + one, + after, + another + ) { + // ... + } + ``` + +- **A vertical indent: empty lines for splitting the code in logical blocks.** + + Even a single function can often be divided in logical blocks. In the example below, the initialization of variables, the main loop and returning the result are split vertically: + + ```js + function pow(x, n) { + let result = 1; + // <-- + for (let i = 0; i < n; i++) { + result *= x; + } + // <-- + return result; + } + ``` + + Insert an extra newline where it helps to make the code more readable. There should not be more than 9 lines of code without a vertical indentation. + +### A semicolon + +A semicolons should be after each statement. Even if could possibly be skipped. + +There are languages where a semicolon is truly optional. It's rarely used there. + +But in JavaScript there are few cases when a line break is sometimes not interpreted as a semicolon. That leaves a place for programming errors, so semicolons should be at place. + +### Nesting levels + +There should not be too many nesting levels. + +Sometimes it's a good idea to use the ["continue"](info:while-for#continue) directive in the loop to evade extra nesting in `if(..) { ... }`: + +Instead of: + +```js +for (let i = 0; i < 10; i++) { + if (cond) { + ... // <- one more nesting level + } +} +``` + +We can write: + +```js +for (let i = 0; i < 10; i++) { + if (!cond) *!*continue*/!*; + ... // <- no extra nesting level +} +``` + +The similar thing can be done with `if/else` and `return`. + +For example, two constructs below are identical. + +The first one: + +```js +function pow(x, n) { + if (n < 0) { + alert("Negative 'n' not supported"); + } else { + let result = 1; + + for (let i = 0; i < n; i++) { + result *= x; + } + + return result; + } +} +``` + +And this: + +```js +function pow(x, n) { + if (n < 0) { + alert("Negative 'n' not supported"); + return; + } + + let result = 1; + + for (let i = 0; i < n; i++) { + result *= x; + } + + return result; +} +``` + +...But the second one is more readable, because the "edge case" of `n < 0` is handled early on, and then we have the "main" code flow, without an additional nesting. + +## Functions below the code + +If you are writing several "helper" functions and the code to use them, then there are three ways to place them. + +1. Functions above the code that uses them: + + ```js + // *!*function declarations*/!* + function createElement() { + ... + } + + function setHandler(elem) { + ... + } + + function walkAround() { + ... + } + + // *!*the code which uses them*/!* + let elem = createElement(); + setHandler(elem); + walkAround(); + ``` +2. Code first, then functions + + ```js + // *!*the code which uses the functions*/!* + let elem = createElement(); + setHandler(elem); + walkAround(); + + // --- *!*helper functions*/!* --- + + function createElement() { + ... + } + + function setHandler(elem) { + ... + } + + function walkAround() { + ... + } + ``` +3. Mixed, a function is described where it's first used. + +Most of time, the second variant is preferred. + +That's because when reading a code, we first want to know "what it does". If the code goes first, then it provides that information. And then maybe we won't need to read functions at all, especially if their names are adequate to what they're doing. + +## Style guides + +A style guide contains general rules about "how to write": which quotes to use, how many spaces to indent, where to put line breaks etc. A lot of minor things. + +In total, when all members of a team use the same style guide, the code looks uniform. No matter who of the team wrote it, still the same style. + +Surely, a team may think out a style guide themselves. But as of now, there's no need to. There are many tried, worked out style guides, easy to adopt. + +For instance: + +- [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml) +- [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) +- [Idiomatic.JS](https://github.com/rwaldron/idiomatic.js) +- (there are more) + +If you're a novice developer, then you could start with the cheatsheet above in the chapter, and later browse the style guides to pick up the common principles and maybe choose one. + +## Automated linters + +There are tools that can check the code style automatically. They are called "linters". + +The great thing about them is that style-checking also finds some bugs, like a typo in variable name or a function. + +So it's recommended to install one, even if you don't want to stick to a "code style". They help to find typos -- and that's already good enough. + +Most well-known tools are: + +- [JSLint](http://www.jslint.com/) -- one of the first linters. +- [JSHint](http://www.jshint.com/) -- more settings than JSHint. +- [ESLint](http://eslint.org/) -- probably the newest one. + +All of them can do the job. The author uses [ESLint](http://eslint.org/). + +Most linters are integrated with editors: just enable the plugin in the editor and configure the style. + +For instance, for ESLint you should do the following: + +1. Install [Node.JS](https://nodejs.org/). +2. Install ESLint with the command `npm install -g eslint` (npm is Node.JS package installer). +3. Create a config file named `.eslintrc` in the root of your JavaScript project (in the folder that contains all your files). + +Here's an example of `.eslintrc`: + +```js +{ + "extends": "eslint:recommended", + "env": { + "browser": true, + "node": true, + "es6": true + }, + "rules": { + "no-console": 0, + }, + "indent": 2 +} +``` + +Here the directive `"extends"` denotes that we base on the "eslint:recommended" set of settings, and then we specify our own. + +Then install/enable the plugin for your editor that integrates with ESLint. The majority of editors have it. + +It is possible to download style rule sets from the web and extend them instead. See for more details about installation. + +Using a linter has the great side-effect. Linters catch typos. For instance, when an undefined variable is accessed, a linter detects it and (if integrated with an editor) highlights. In most cases that's a mistype. So we can fix it right ahead. + +For that reason even if you're not concerned about styles, using a linter is highly recommended. + +Also certain IDEs support built-in linting, but not so tunable as ESLint. + +## Summary + +All syntax rules from this chapter and the style guides aim to increase readability, so all of them are debatable. + +When we think about "how to write better?", the sole criterion is "what makes the code more readable and easier to understand? what helps to evade errors?" That's the main thing to keep in mind when choosing the style or discussing which one is better. + +Read style guides to see the latest ideas about that and follow those that you find the best. diff --git a/1-js/03-code-quality/02-coding-style/code-style.png b/1-js/03-code-quality/02-coding-style/code-style.png new file mode 100644 index 00000000..278fd294 Binary files /dev/null and b/1-js/03-code-quality/02-coding-style/code-style.png differ diff --git a/1-js/03-code-quality/02-coding-style/code-style@2x.png b/1-js/03-code-quality/02-coding-style/code-style@2x.png new file mode 100644 index 00000000..b5761434 Binary files /dev/null and b/1-js/03-code-quality/02-coding-style/code-style@2x.png differ diff --git a/1-js/03-code-quality/02-coding-style/figure-bracket-style.png b/1-js/03-code-quality/02-coding-style/figure-bracket-style.png new file mode 100644 index 00000000..112c2803 Binary files /dev/null and b/1-js/03-code-quality/02-coding-style/figure-bracket-style.png differ diff --git a/1-js/03-code-quality/02-coding-style/figure-bracket-style@2x.png b/1-js/03-code-quality/02-coding-style/figure-bracket-style@2x.png new file mode 100644 index 00000000..ce6e75c4 Binary files /dev/null and b/1-js/03-code-quality/02-coding-style/figure-bracket-style@2x.png differ diff --git a/1-js/03-code-quality/03-comments/article.md b/1-js/03-code-quality/03-comments/article.md new file mode 100644 index 00000000..8fa35a9f --- /dev/null +++ b/1-js/03-code-quality/03-comments/article.md @@ -0,0 +1,150 @@ +# Comments + +Comments are generally a good thing. But novices in programming generally get that wrong. They write comments explaining "what is going on in the code". + +But the amount of such "explanatory" comments should be minimal. + +Seriously, a good code should be easy to understand without them. + +There's a great rule about that: "if the code is so unclear that it requires a comment, then maybe it should be rewritten instead". + +[cut] + +Sometimes it's beneficial to replace a code piece with a function, like here: + +```js +function showPrimes(n) { + nextPrime: + for (let i = 2; i < n; i++) { + +*!* + // check if i is a prime number + for (let j = 2; j < i; j++) { + if (i % j == 0) continue nextPrime; + } +*/!* + + alert(i); + } +} +``` + +The better variant: + + +```js +function showPrimes(n) { + + for (let i = 2; i < n; i++) { + *!*if (!isPrime(i)) continue;*/!* + + alert(i); + } +} + +function isPrime(n) { + for (let i = 2; i < n; i++) { + if ( n % i == 0) return false; + } + return true; +} +``` + +Now we can understand the code easily without the comment. Such code is called *self-descriptive*. + +And if we have a long "code sheet" like this: + +```js +// here we add whiskey +for(let i = 0; i < 10; i++) { + let drop = getWhiskey(); + smell(drop); + add(drop, glass); +} + +// here we add juice +for(let t = 0; t < 3; t++) { + let tomato = getTomato(); + examine(tomato); + let juice = press(tomato); + add(juice, glass); +} + +// ... +``` + +Then it might be a better variant to refactor it into functions like: + +```js +addWhiskey(glass); +addJuice(glass); + +function addWhiskey(container) { + for(let i = 0; i < 10; i++) { + let drop = getWhiskey(); + //... + } +} + +function addJuice(container) { + for(let t = 0; t < 3; t++) { + let tomato = getTomato(); + //... + } +} +``` + +That's readable without comments. And also the code structure is better when split. It's clear what every function does, what it takes and what it returns. + +In reality, we can't totally evade "explanatory" comments. There are complex algorithms. And there are smart "tweaks" for purposes of optimization. But generally we should try to keep the code simple and self-descriptive. + +## Good comments + +Which comments are good? + +Describe the architecture +: Provide a high-level overview of components, how they interact, what's the control flow in various situations... In short -- the bird's eye view of the code. There's a special diagram language [UML](http://wikipedia.org/wiki/Unified_Modeling_Language) for high-level architecture diagrams. Definitely worth studying. + +Document a function 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) { + ... + } + ``` + + Such comments allow 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. + + 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 the task is solved this way? +: What's written is important. But what's *not* written maybe even more important to understand what's going on. Why the task is solved exactly this way? The code gives no answer. + + If there are many ways to solve the task, why this one? Especially when it's not the most obvious one. + + Without such comments the following situation is possible: + 1. You (or your colleague) open the code written some time ago, and see that it's "suboptimal". + 2. You think: "How stupid I was then, and how much smarter I'm now", and rewrite using the "more obvious and correct" variant. + 3. ...The urge to rewrite was good. But in the process you see that the "more obvious" solution is actually lacking. You even dimly remember why, because you already tried it long ago. You revert to the correct variant, but the time was wasted. + + Comments that explain the solution are very important. They help to continue development the right way. + +Any subtle features of the code? Where they are used? +: If the code has anything subtle and counter-obvious, it's definitely worth commenting. + +## Summary + +One of signs of a good developer is his comments. Good comments allow to maintain the code well, return to it after a long delay and use features more effectively. + +Comments are also used for auto-documenting tools: they read them and generate HTML-docs (or in another format). diff --git a/1-js/03-code-quality/04-ninja-code/article.md b/1-js/03-code-quality/04-ninja-code/article.md new file mode 100644 index 00000000..ee18ec53 --- /dev/null +++ b/1-js/03-code-quality/04-ninja-code/article.md @@ -0,0 +1,228 @@ +# How to write bad code? + +Programmer ninjas of the past used these tricks to make code maintainers cry. Code review gurus look for them in test tasks. Novice developers sometimes use them even better than programmer ninjas. + +Read them carefully and find out who you are -- a ninja, a novice, or maybe a code reviewer? + +[cut] + +```warn header="Irony detected" +These are rules of writing bad code. Just... You know, some people miss the point. +``` + +## Brevity is the soul of wit + +Make the code as short as possible. Show how smart you are. + +Let subtle language features guide you. + +For instance, take a look at this ternary operator `'?'`: + +```js +// taken from a well-known javascript library +i = i ? i < 0 ? Math.max(0, len + i) : i : 0; +``` + +Cool, right? If you write like that, the developer who comes across this line and tries to understand what is the value of `i` will probably have a merry time. Then come to you, seeking for an answer. + +Tell him that shorter is always better. Initiate him into the paths of ninja. + +## One-letter variables + +```quote author="Laozi (Tao Te Ching)" +The Dao hides in wordlessness. Only the Dao is well begun and well +completed. +``` + +Another way to code faster (and much worse!) 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 the "search" of the editor. And even if someone does, he won't be able to "decipher" what the name `a` or `b` means. + +...But there's an exception. A real ninja will never use `i` as the counter in a `"for"` loop. Anywhere, but not here. Look around, there are so much more exotic letters. For instance, `x` or `y`. + +An exotic variable as a loop counter is especially cool if the loop body takes 1-2 pages (make it longer if you can). Then if someone looks deep inside the loop, he won't be able to figure out fast that the variable is the loop counter. + +## Use abbreviations + +If the team rules forbid to use one-letter and vague names -- shorten them, make abbreviations. + +Like this: + +- `list` -> `lst`. +- `userAgent` -> `ua`. +- `browser` -> `brsr`. +- ...etc + +Only the one with a truly good intuition will be able to understand all such names. Try to shorten everything. Only a worthy person will be able to uphold the development of such code. + +## Soar high. Be abstract. + +```quote author="Laozi (Tao Te Ching)" +The great square is cornerless
+The great vessel is last complete,
+The great note is rarified sound,
+The great image has no form. +``` + +While choosing a name try to use the most abstract word. Like `obj`, `data`, `value`, `item`, `elem` and so on. + +- **The ideal name for a variable is `data`.** Use it everywhere where you can. Indeed, every variable holds *data*, right? + + ...But what to do if `data` is already taken? Try `value`, it's also universal. A variable always has a *value*, correct? + +- **Name the variable by its type: `str`, `num`...** + + ...But will that make the code worse? Actually, yes! + + From one hand, the variable name still means something. It says what's inside the variable: a string, a number or something else. But when an outsider tries to understand the code -- he'll be surprised to see that there's actually no information at all! + + Actually, the value type is easy to see by debugging. But what's the meaning of the variable? Which string/number it stores? There's just no way to figure out without a good meditation! + +- **...But what if there are no more such names?** Just add a letter: `item1, item2, elem5, data1`... + +## Attention test + +Only a truly attentive programmer should be able to understand the code. But how to check that? + +**One of the ways -- use similar variable names, like `date` and `data`.** + +Mix them where you can. + +A quick read of such code becomes impossible. And when there's a typo... Ummm... We're stuck for long, time to drink tea. + + +## Smart synonyms + +```quote author="Confucius" +The hardest thing of all is to find a black cat in a dark room, especially if there is no cat. +``` + +Use *similar* names for *same* things, that makes life more interesting and shows your creativity to the public. + +For instance, the function prefixes. If a function shows a message on the screen -- start it with `display…`, like `displayMessage`. And then if another function shows something else, like a user name, start it with `show…` (like `showName`). + +Insinuate that there's a subtle difference difference between such functions, while there is none. + +Make a pact with fellow ninjas of the team: if John starts "showing" functions with `display...` in his code, then Peter could use `render..`, and Ann -- `paint...`. Note how more interesting and diverse the code became. + +...And now the hat trick! + +For two functions with important differences -- use the same prefix! + +For instance, the function `printPage(page)` will use a printer. And the function `printText(text)` will put the text on-screen. Let an unfamiliar reader think well over things: "Where does `printMessage(message)` put the message? To a printer or on the screen?". To make it really shine, `printMessage(message)` should output it in the new window! + +## Reuse names + +```quote author="Laozi (Tao Te Ching)" +Once the whole is divided, the parts
+need names.
+There are already enough names.
+One must know when to stop. +``` + +Add a new variable only when absolutely necessary. + +Instead, reuse existing names. Just write new values into them. + +In a function try to use only variables passed as parameters. + +That would make it impossible to identify what's exactly in the variable *now*. And also where it comes from. A person with weak intuition would have to analyze the code line-by-line and track the changes through every code branch. + +**An advanced variant of the approach is to covertly (!) replace the value with something alike in the middle of a loop or a function.** + +For instance: + +```js +function ninjaFunction(elem) { + // 20 lines of code working with elem + + elem = clone(elem); + + // 20 more lines, now working with the clone of the elem! +} +``` + +A fellow programmer who wants to work with `elem` in the second half of the function will be surprised... Only during the debugging, after examining the code he will find out that he's working with a clone! + +Deadly effective even against an experienced ninja. Seen in code regularly. + +## Underscores for fun + +Put underscores `_` and `__` before variable names. Like `_name` or `__value`. It would be great if only you know their meaning. Or, better, without meaning at all. + +You kill two rabbits with one shot. First, the code becomes longer and less readable, and the second, a fellow developer may spend a long time trying to figure out what the underscores mean. + +A smart ninja puts underscores at one spot of code and evades them at other places. That makes the code even more fragile and increases the probability of future errors. + +## Show your love + +Let everyone see how magnificent your entities are! Names like `superElement`, `megaFrame` and `niceItem` will definitely enlighten a reader. + +Indeed, from one hand, something is written: `super..`, `mega..`, `nice..` But from the other hand -- that brings no details. A reader may decide to look for a hidden meaning and meditate for an hour or two. + +## Overlap outer variables + +```quote author="Guan Yin Zi" +When in the light, can't see anything in the darkness.
+When in the darkness, can see everything in the light. +``` + +Use same names for variables inside and outside a function. As simple. No efforts required. + +```js +let *!*user*/!* = authenticateUser(); + +function render() { + let *!*user*/!* = anotherValue(); + ... + ...many lines... + ... + ... // <-- a programmer wants to work with user here and... + ... +} +``` + +A programmer who jumps inside the `render` will probably miss to notice that there's a local `user` shadowing the outer one. + +Then he'll try to work with `user` it assuming that it's the external variable, the result of `authenticateUser()`... The trap is sprung! Hello, debugger... + + +## Side-effects everywhere! + +There are functions that look like they don't change anything. Like `isReady()`, `checkPermission()`, `findTags()`... They are assumed to carry out calculations, find and return the data, without changing anything outside of them. That's called "no side-effects". + +**A really beautiful trick -- is to add a "useful" action to them, besides the main task.** + +The expression of dazed surprise on the face of your colleague when he see a function named `is..`, `check..` or `find...` changing something -- will definitely broaden your boundaries of reason. + +**Another way to surprise -- is to return a non-standard result.** + +Show your original thinking! Let the call of `checkPermission` return not `true/false`, but a complex object with the results of the check. + +Those developers who try to write `if (checkPermission(..))`, will wonder why it doesn't work. Tell them: "Read the docs!". And give this article. + + +## Powerful functions! + +```quote author="Laozi (Tao Te Ching)" +The great Tao flows everywhere,
+both to the left and to the right. +``` + +Don't limit the function by what's written in its name. Be broader. + +For instance, a function `validateEmail(email)` could (besides checking the email for correctness) show an error message and ask to re-enter the email. + +Additional actions should not be obvious from the function name. A true ninja coder will make them not obvious from the code as well. + +**Joining several actions into one protects your code from reuse.** + +Imagine, another developer wants only to check the email, and not output any message. Your function `validateEmail(email)` that does both will not suit him. So he won't break your meditation by asking anything about it. + +## Summary + +All "pieces of advice" above are from the real code... Sometimes, written by experienced developers. Maybe even more experienced than you are ;) + +- Follow some of them -- and your code will become full of surprises. +- Follow many of them -- and your code will become truly yours, no one would want to change it. +- Follow all -- and your code will become a valuable lesson for young developers looking for enlightment. diff --git a/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/solution.md b/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/solution.md new file mode 100644 index 00000000..7b58f0bf --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/solution.md @@ -0,0 +1,50 @@ +The test demonstrates one of the temptations a developer meets when writing tests. + +What we have here is actually 3 tests, but layed out as a single function with 3 asserts. + +Sometimes it's easier to write this way, but if an error occurs, it's much less obvious what went wrong. + +If an error happens inside a complex execution flow, then we'll have to figure out the data at that point. We'll actually have to *debug the test*. + +It would be much better to break the test into multiple `it` blocks with clearly written inputs and outputs. + +Like this: +```js +describe("Raises x to power n", function() { + it("5 in the power of 1 equals 5", function() { + assert.equal(pow(5, 1), 5); + }); + + it("5 in the power of 2 equals 25", function() { + assert.equal(pow(5, 2), 25); + }); + + it("5 in the power of 3 equals 125", function() { + assert.equal(pow(5, 3), 125); + }); +}); +``` + +We replaced the single `it` with `describe` and a group of `it` blocks. Now if something fails we would see clearly what the data was. + +Also we can isolate a single test and run it in standalone mode by writing `it.only` instead of `it`: + + +```js +describe("Raises x to power n", function() { + it("5 in the power of 1 equals 5", function() { + assert.equal(pow(5, 1), 5); + }); + +*!* + // Mocha will run only this block + it.only("5 in the power of 2 equals 25", function() { + assert.equal(pow(5, 2), 25); + }); +*/!* + + it("5 in the power of 3 equals 125", function() { + assert.equal(pow(5, 3), 125); + }); +}); +``` diff --git a/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/task.md b/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/task.md new file mode 100644 index 00000000..66fece09 --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/task.md @@ -0,0 +1,24 @@ +importance: 5 + +--- + +# What's wrong in the test? + +What's wrong in the test of `pow` below? + +```js +it("Raises x to the power n", function() { + let x = 5; + + let result = x; + assert.equal(pow(x, 1), result); + + result *= x; + assert.equal(pow(x, 2), result); + + result *= x; + assert.equal(pow(x, 3), result); +}); +``` + +P.S. Syntactically the test is correct and passes. diff --git a/1-js/03-code-quality/05-testing-mocha/article.md b/1-js/03-code-quality/05-testing-mocha/article.md new file mode 100644 index 00000000..03394260 --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/article.md @@ -0,0 +1,412 @@ +# Automated testing with mocha + +Automated testing will be used further in tasks. + +It's actually a part of the "educational minimum" of a developer. + +[cut] + +## Why we need tests? + +When we write a function, we usually can imagine what it should do: which parameters give which results. + +During the development, we can check the function by running it and comparing the outcome with the expected one. For instance, we can do it in the console. + +If something's wrong -- then we fix the code, run again, check the result -- and so on till it works. + +But such manual "re-runs" are imperfect. + +**When testing a code by manual re-runs -- it's easy to miss something.** + +For instance, we're creating a function `f`. Wrote some code, testing: `f(1)` works, but `f(2)` doesn't work. We fix the code and now `f(2)` works. Looks complete? But we forgot to re-test `f(1)`. That may lead to an error. + +That's very typical. When we develop something, we keep a lot of possible use cases and mind. But it's hard to expect from programmer to check all of them manually after every change. So it becomes easy to fix one thing and break another one. + +**Automated testing means that tests are written separately, in addition to the code. They can be executed easily and check all the main use cases.** + +## Behavior Driven Development (BDD) + +Let's use a technique named [Behavior Driven Development](http://en.wikipedia.org/wiki/Behavior-driven_development) or, in short, BDD. That approach is used among many projects. BDD is not just about testing. That's more. + +**BDD is three things in one: tests AND documentation AND examples.** + +Enough words. Let's see the example. + +## Development of "pow": the spec + +Let's say we want to make a function `pow(x, n)` that raises `x` to an integer power `n`. We assume that `n≥0`. + +That task is just an example: there's `**` operator in JavaScript that can do that, but here we concentrate on the development flow that can be applied to more complex tasks as well. + +Before creating the code of `pow`, we can imagine what the function should do and describe it. + +Such description is called a *specification* or, in short, a spec, and looks like this: + +```js +describe("pow", function() { + + it("raises to n-th power", function() { + assert.equal(pow(2, 3), 8); + }); + +}); +``` + +A spec has three main building blocks that you can see above: + +`describe("title", function() { ... })` +: What functionality we're describing. Uses to group "workers" -- the `it` blocks. In our case we're describing the function `pow`. + +`it("title", function() { ... })` +: In the title of `it` we *in a human-readable way* describe the particular use case, and the second argument is a function that tests it. + +`assert.equal(value1, value2)` +: The code inside `it` block, if the implementation is correct, should execute without errors. + + Functions `assert.*` are used to check whether `pow` works as expected. Right here we're using one of them -- `assert.equal`, it compares arguments and yields an error if they are not equal. Here it checks that the result of `pow(2, 3)` equals `8`. + + There are other types of comparisons and checks that we'll see further. + +## The development flow + +The flow of development usually looks like this: + +1. An initial spec is written, with tests for the most basic functionality. +2. An initial implementation is created. +3. To check whether it works, we run the testing framework [Mocha](http://mochajs.org/) (more details soon) that runs the spec. Errors are displayed. We make corrections till everything works. +4. Now we have a working initial implementation with tests. +5. We add more use cases to the spec, probably not yet supported by the implementations. Tests start to fail. +6. Go to 3, update the implementation till tests give no errors. +7. Repeat steps 3-6 till the functionality is ready. + +So, the development is *iterative*. We write the spec, implement it, make sure tests pass, then write more tests, make sure they work etc. At the end we have both a working implementation and tests for it. + +In our case, the first step is complete: we have an initial spec for `pow`. So let's make an implementation. But before that let's make a "zero" run of the spec, just to see that tests are working (they will all fail). + +## The spec in action + +Here in the tutorial we'll be using the following JavaScript libraries for tests: + +- [Mocha](http://mochajs.org/) -- the core framework: it provides common testing functions including `describe` and `it` and the main function that runs tests. +- [Chai](http://chaijs.com) -- the library with many assertions. It allows to use a lot of different assertions, for now we need only `assert.equal`. +- [Sinon](http://sinonjs.org/) -- a library to spy over functions, emulate built-in functions and more, we'll need it much later. + +These libraries are suitable for both in-browser and server-side testing. Here we'll consider the browser variant. + +The full HTML page with these frameworks and `pow` spec: + +```html src="index.html" +``` + +The page can be divided into four parts: + +1. The `` -- add third-party libraries and styles for tests. +2. The ` + + + + + + + + + + + + +
+ + + + + + 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 new file mode 100644 index 00000000..cad51d3e --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/beforeafter.view/test.js @@ -0,0 +1,12 @@ +describe("test", function() { + + before(() => alert("Testing started – before all tests")); + after(() => alert("Testing finished – after all tests")); + + beforeEach(() => alert("Before a test – enter a test")); + afterEach(() => alert("After a test – exit a test")); + + it('test 1', () => alert(1)); + it('test 2', () => alert(2)); + +}); diff --git a/1-js/03-code-quality/05-testing-mocha/index.html b/1-js/03-code-quality/05-testing-mocha/index.html new file mode 100644 index 00000000..28a2ea62 --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + diff --git a/1-js/03-code-quality/05-testing-mocha/pow-1.view/index.html b/1-js/03-code-quality/05-testing-mocha/pow-1.view/index.html new file mode 100644 index 00000000..e48a8d3a --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-1.view/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + diff --git a/1-js/03-code-quality/05-testing-mocha/pow-1.view/test.js b/1-js/03-code-quality/05-testing-mocha/pow-1.view/test.js new file mode 100644 index 00000000..89ba412e --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-1.view/test.js @@ -0,0 +1,7 @@ +describe("pow", function() { + + it("raises to n-th power", function() { + assert.equal(pow(2, 3), 8); + }); + +}); diff --git a/1-js/03-code-quality/05-testing-mocha/pow-2.view/index.html b/1-js/03-code-quality/05-testing-mocha/pow-2.view/index.html new file mode 100644 index 00000000..d82a79dc --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-2.view/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + 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 new file mode 100644 index 00000000..9a2f8fde --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-2.view/test.js @@ -0,0 +1,11 @@ +describe("pow", function() { + + it("2 raised to power 3 is 8", function() { + assert.equal(pow(2, 3), 8); + }); + + it("3 raised to power 3 is 27", function() { + assert.equal(pow(3, 3), 27); + }); + +}); diff --git a/1-js/03-code-quality/05-testing-mocha/pow-3.view/index.html b/1-js/03-code-quality/05-testing-mocha/pow-3.view/index.html new file mode 100644 index 00000000..c71b0d5d --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-3.view/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + diff --git a/1-js/03-code-quality/05-testing-mocha/pow-3.view/test.js b/1-js/03-code-quality/05-testing-mocha/pow-3.view/test.js new file mode 100644 index 00000000..8663952a --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-3.view/test.js @@ -0,0 +1,14 @@ +describe("pow", function() { + + function makeTest(x) { + let expected = x * x * x; + it(`${x} in the power 3 is ${expected}`, function() { + assert.equal(pow(x, 3), expected); + }); + } + + for (let x = 1; x <= 5; x++) { + makeTest(x); + } + +}); diff --git a/1-js/03-code-quality/05-testing-mocha/pow-4.view/index.html b/1-js/03-code-quality/05-testing-mocha/pow-4.view/index.html new file mode 100644 index 00000000..c71b0d5d --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-4.view/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + diff --git a/1-js/03-code-quality/05-testing-mocha/pow-4.view/test.js b/1-js/03-code-quality/05-testing-mocha/pow-4.view/test.js new file mode 100644 index 00000000..10a032d0 --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-4.view/test.js @@ -0,0 +1,19 @@ +describe("pow", function() { + + describe("raises x to power n", function() { + + function makeTest(x) { + let expected = x * x * x; + it(`${x} in the power 3 is ${expected}`, function() { + assert.equal(pow(x, 3), expected); + }); + } + + for (let x = 1; x <= 5; x++) { + makeTest(x); + } + + }); + + // ... more tests to follow here, both describe and it can be added +}); diff --git a/1-js/03-code-quality/05-testing-mocha/pow-full.view/index.html b/1-js/03-code-quality/05-testing-mocha/pow-full.view/index.html new file mode 100644 index 00000000..076b1e5a --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-full.view/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + diff --git a/1-js/03-code-quality/05-testing-mocha/pow-full.view/test.js b/1-js/03-code-quality/05-testing-mocha/pow-full.view/test.js new file mode 100644 index 00000000..a5a34597 --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-full.view/test.js @@ -0,0 +1,26 @@ +describe("pow", function() { + + describe("raises x to power n", function() { + + function makeTest(x) { + let expected = x * x * x; + it(`${x} in the power 3 is ${expected}`, function() { + assert.equal(pow(x, 3), expected); + }); + } + + for (let x = 1; x <= 5; x++) { + makeTest(x); + } + + }); + + it("if n is negative, the result is NaN", function() { + assert.isNaN(pow(2, -1)); + }); + + it("if n is not integer, the result is NaN", function() { + assert.isNaN(pow(2, 1.5)); + }); + +}); diff --git a/1-js/03-code-quality/05-testing-mocha/pow-min.view/index.html b/1-js/03-code-quality/05-testing-mocha/pow-min.view/index.html new file mode 100644 index 00000000..d82a79dc --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-min.view/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + diff --git a/1-js/03-code-quality/05-testing-mocha/pow-min.view/test.js b/1-js/03-code-quality/05-testing-mocha/pow-min.view/test.js new file mode 100644 index 00000000..89ba412e --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-min.view/test.js @@ -0,0 +1,7 @@ +describe("pow", function() { + + it("raises to n-th power", function() { + assert.equal(pow(2, 3), 8); + }); + +}); diff --git a/1-js/03-code-quality/05-testing-mocha/pow-nan.view/index.html b/1-js/03-code-quality/05-testing-mocha/pow-nan.view/index.html new file mode 100644 index 00000000..523ae25e --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-nan.view/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + diff --git a/1-js/03-code-quality/05-testing-mocha/pow-nan.view/test.js b/1-js/03-code-quality/05-testing-mocha/pow-nan.view/test.js new file mode 100644 index 00000000..a5a34597 --- /dev/null +++ b/1-js/03-code-quality/05-testing-mocha/pow-nan.view/test.js @@ -0,0 +1,26 @@ +describe("pow", function() { + + describe("raises x to power n", function() { + + function makeTest(x) { + let expected = x * x * x; + it(`${x} in the power 3 is ${expected}`, function() { + assert.equal(pow(x, 3), expected); + }); + } + + for (let x = 1; x <= 5; x++) { + makeTest(x); + } + + }); + + it("if n is negative, the result is NaN", function() { + assert.isNaN(pow(2, -1)); + }); + + it("if n is not integer, the result is NaN", function() { + assert.isNaN(pow(2, 1.5)); + }); + +}); diff --git a/1-js/03-code-quality/06-polyfills/article.md b/1-js/03-code-quality/06-polyfills/article.md new file mode 100644 index 00000000..37ea9f89 --- /dev/null +++ b/1-js/03-code-quality/06-polyfills/article.md @@ -0,0 +1,57 @@ + +# Polyfills + +The JavaScript language steadily evolves. New proposals to the language appear regularly, they are analyzed and, if considered worthy, are appended to the list at and then progress to the [specification](http://www.ecma-international.org/publications/standards/Ecma-262.htm). + +Teams behind JavaScript engines have their own ideas about what to implement first. They may decide to implement proposals that are in draft and postpone things that are already in the spec, because they are less interesting or just harder to do. + +So it's quite common for an engine to implement only the part of the standard. + +A good page to see the current state of support for language features is (it's big, we have a lot to study yet). + +## Babel + +When we use modern features of the language, some engines may fail to support such code. Just as said, not all features are implemented everywhere. + +Here Babel comes to the rescue. + +[Babel](https://babeljs.io) is a [transpiler](https://en.wikipedia.org/wiki/Source-to-source_compiler). It rewrites modern JavaScript code into the previous standard. + +Actually, there are two parts in Babel: + +1. First, the transpiler program, which rewrites the code. The developer runs it on his own computer. It rewrites the code into the older standard. And then the code is delivered to the website for users. Modern project build system like [webpack](http://webpack.github.io/) or [brunch](http://brunch.io/) provide means to run transpiler automatically on every code change, so that doesn't involve any time loss from our side. + +2. Second, the polyfill. + + The transpiler rewrites the code, so syntax features are covered. But for new functions we need to write a special script that implements them. JavaScript is a highly dynamic language, scripts may not just add new functions, but also modify built-in ones, so that they behave according to the modern standard. + + There's a term "polyfill" for scripts that "fill in" the gap and add missing implementations. + + Two interesting polyfills are: + - [babel polyfill](https://babeljs.io/docs/usage/polyfill/) that supports a lot, but is big. + - [polyfill.io](http://polyfill.io) service that allows to load/construct polyfills on-demand, depending on the features we need. + +So, we need to setup the transpiler and add the polyfill for old engines to support modern features. + +If we orient towards modern engines and do not use features except those supported everywhere, then we don't need to use Babel. + +## Examples in the tutorial + + +````online +Most examples are runnable at-place, like this: + +```js run +alert('Press the "Play" button in the upper-right corner to run'); +``` + +Examples that use modern JS will work only if your browser supports it. +``` + +```offline +As you're reading the offline version, examples are not runnable. But they usually work :) +``` + +[Chrome Canary](https://www.google.com/chrome/browser/canary.html) is good for all examples, but other modern browsers are mostly fine too. + +Note that on production we can use Babel to translate the code into suitable for less recent browsers, so there will be no such limitation, the code will run everywhere. diff --git a/1-js/03-code-quality/index.md b/1-js/03-code-quality/index.md new file mode 100644 index 00000000..2ef64fa6 --- /dev/null +++ b/1-js/03-code-quality/index.md @@ -0,0 +1,3 @@ +# Code quality + +This chapter explains coding practices that we'll use further in the development. diff --git a/1-js/04-object-basics/01-object/2-hello-object/solution.md b/1-js/04-object-basics/01-object/2-hello-object/solution.md new file mode 100644 index 00000000..60083b96 --- /dev/null +++ b/1-js/04-object-basics/01-object/2-hello-object/solution.md @@ -0,0 +1,10 @@ + + +```js +let user = {}; +user.name = "John"; +user.surname = "Smith"; +user.name = "Pete"; +delete user.name; +``` + diff --git a/1-js/04-object-basics/01-object/2-hello-object/task.md b/1-js/04-object-basics/01-object/2-hello-object/task.md new file mode 100644 index 00000000..2841a058 --- /dev/null +++ b/1-js/04-object-basics/01-object/2-hello-object/task.md @@ -0,0 +1,14 @@ +importance: 5 + +--- + +# Hello, object + +Write the code, one line for each action: + +1. Create an empty object `user`. +2. Add the property `name` with the value `John`. +3. Add the property `surname` with the value `Smith`. +4. Change the value of the `name` to `Pete`. +5. Remove the property `name` from the object. + diff --git a/1-js/04-object-basics/01-object/3-is-empty/_js.view/solution.js b/1-js/04-object-basics/01-object/3-is-empty/_js.view/solution.js new file mode 100644 index 00000000..e7f63284 --- /dev/null +++ b/1-js/04-object-basics/01-object/3-is-empty/_js.view/solution.js @@ -0,0 +1,7 @@ +function isEmpty(obj) { + for (let key in obj) { + // if the loop has started, there is a prorty + return false; + } + return true; +} \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/3-is-empty/_js.view/test.js b/1-js/04-object-basics/01-object/3-is-empty/_js.view/test.js new file mode 100644 index 00000000..4db5efab --- /dev/null +++ b/1-js/04-object-basics/01-object/3-is-empty/_js.view/test.js @@ -0,0 +1,11 @@ +describe("isEmpty", function() { + it("returns true for an empty object", function() { + assert.isTrue(isEmpty({})); + }); + + it("returns false if a property exists", function() { + assert.isFalse(isEmpty({ + anything: false + })); + }); +}); \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/3-is-empty/solution.md b/1-js/04-object-basics/01-object/3-is-empty/solution.md new file mode 100644 index 00000000..9e8b3d90 --- /dev/null +++ b/1-js/04-object-basics/01-object/3-is-empty/solution.md @@ -0,0 +1,11 @@ +Just loop over the object and `return false` immediately if there's at least one property. + +```js +function isEmpty(obj) + for(let key in obj) { + return false; + } + return true; +} +``` + diff --git a/1-js/04-object-basics/01-object/3-is-empty/task.md b/1-js/04-object-basics/01-object/3-is-empty/task.md new file mode 100644 index 00000000..c438d36a --- /dev/null +++ b/1-js/04-object-basics/01-object/3-is-empty/task.md @@ -0,0 +1,20 @@ +importance: 5 + +--- + +# Check for emptiness + +Write the function `isEmpty(obj)` which returns `true` if the object has no properties, `false` otherwise. + +Should work like that: + +```js +let schedule = {}; + +alert( isEmpty(schedule) ); // true + +schedule["8:30"] = "get up"; + +alert( isEmpty(schedule) ); // false +``` + 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 new file mode 100644 index 00000000..f73c2f92 --- /dev/null +++ b/1-js/04-object-basics/01-object/4-const-object/solution.md @@ -0,0 +1,19 @@ +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 new file mode 100644 index 00000000..95f7a64d --- /dev/null +++ b/1-js/04-object-basics/01-object/4-const-object/task.md @@ -0,0 +1,18 @@ +importance: 5 + +--- + +# Constant objects? + +Is it possible to change an object declared with `const`, how do you think? + +```js +const user = { + name: "John" +}; + +*!* +// does it work? +user.name = "Pete"; +*/!* +``` diff --git a/1-js/04-object-basics/01-object/5-sum-object/solution.md b/1-js/04-object-basics/01-object/5-sum-object/solution.md new file mode 100644 index 00000000..eb5b176a --- /dev/null +++ b/1-js/04-object-basics/01-object/5-sum-object/solution.md @@ -0,0 +1,16 @@ + +```js run +let salaries = { + John: 100, + Ann: 160, + Pete: 130 +} + +let sum = 0; +for(let key in salaries) { + sum += salaries[key]; +} + +alert(sum); // 390 +``` + diff --git a/1-js/04-object-basics/01-object/5-sum-object/task.md b/1-js/04-object-basics/01-object/5-sum-object/task.md new file mode 100644 index 00000000..7e3e048d --- /dev/null +++ b/1-js/04-object-basics/01-object/5-sum-object/task.md @@ -0,0 +1,19 @@ +importance: 5 + +--- + +# Sum object properties + +We have an object storing salaries of our team: + +```js +let salaries = { + John: 100, + Ann: 160, + Pete: 130 +} +``` + +Write the code to sum all salaries and store in the variable `sum`. Should be `390` in the example above. + +If `salaries` is empty, then the result must be `0`. \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/8-multiply-numeric/_js.view/solution.js b/1-js/04-object-basics/01-object/8-multiply-numeric/_js.view/solution.js new file mode 100644 index 00000000..4668c1dc --- /dev/null +++ b/1-js/04-object-basics/01-object/8-multiply-numeric/_js.view/solution.js @@ -0,0 +1,7 @@ +function multiplyNumeric(obj) { + for (let key in obj) { + if (typeof obj[key] == 'number') { + obj[key] *= 2; + } + } +} \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/8-multiply-numeric/_js.view/source.js b/1-js/04-object-basics/01-object/8-multiply-numeric/_js.view/source.js new file mode 100644 index 00000000..a02b1e1c --- /dev/null +++ b/1-js/04-object-basics/01-object/8-multiply-numeric/_js.view/source.js @@ -0,0 +1,17 @@ +let menu = { + width: 200, + height: 300, + title: "My menu" +}; + + +function multiplyNumeric(obj) { + + /* your code */ + +} + +multiplyNumeric(menu); + +alert( "menu width=" + menu.width + " height=" + menu.height + " title=" + menu.title ); + diff --git a/1-js/04-object-basics/01-object/8-multiply-numeric/_js.view/test.js b/1-js/04-object-basics/01-object/8-multiply-numeric/_js.view/test.js new file mode 100644 index 00000000..064e5414 --- /dev/null +++ b/1-js/04-object-basics/01-object/8-multiply-numeric/_js.view/test.js @@ -0,0 +1,18 @@ +describe("multiplyNumeric", function() { + it("multiplies all numeric properties by 2", function() { + let menu = { + width: 200, + height: 300, + title: "My menu" + }; + let result = multiplyNumeric(menu); + assert.equal(menu.width, 400); + assert.equal(menu.height, 600); + assert.equal(menu.title, "My menu"); + }); + + it("returns nothing", function() { + assert.isUndefined( multiplyNumeric({}) ); + }); + +}); diff --git a/2-ui/1-document/14-styles-and-classes/2-create-notification/solution.md b/1-js/04-object-basics/01-object/8-multiply-numeric/solution.md similarity index 100% rename from 2-ui/1-document/14-styles-and-classes/2-create-notification/solution.md rename to 1-js/04-object-basics/01-object/8-multiply-numeric/solution.md diff --git a/1-js/04-object-basics/01-object/8-multiply-numeric/task.md b/1-js/04-object-basics/01-object/8-multiply-numeric/task.md new file mode 100644 index 00000000..33eb8922 --- /dev/null +++ b/1-js/04-object-basics/01-object/8-multiply-numeric/task.md @@ -0,0 +1,33 @@ +importance: 3 + +--- + +# Multiply numeric properties by 2 + +Create a function `multiplyNumeric(obj)` that multiplies all numeric properties of `obj` by `2`. + +For instance: + +```js +// before the call +let menu = { + width: 200, + height: 300, + title: "My menu" +}; + +multiplyNumeric(menu); + +// after the call +menu = { + width: 400, + height: 600, + title: "My menu" +}; +``` + +Please note that `multiplyNumeric` does not need to return anything. It should modify the object in-place. + +P.S. Use `typeof` to check for a number here. + + diff --git a/1-js/04-object-basics/01-object/article.md b/1-js/04-object-basics/01-object/article.md new file mode 100644 index 00000000..b2d612a5 --- /dev/null +++ b/1-js/04-object-basics/01-object/article.md @@ -0,0 +1,737 @@ + +# Objects + +As we know from the chapter , there are 7 language types in JavaScript. Six of them are called "primitive", because their values contain only a single thing (be it a string or a number or whatever). + +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. + +[cut] + +An object can be created with figure brackets `{…}` with an optional list of *properties*. A property is a "key: value" pair, where `key` is a string (also called a "property name"), and `value` can be anything. + +We can imagine an object as a cabinet with signed files. Every piece of data is stored in it's file by the key. It's easy to find a file by it's name or add/remove a file. + +![](object.png) + +An empty object ("empty cabinet") can be created using one of two syntaxes: + +```js +let user = new Object(); // "object constructor" syntax +let user = {}; // "object literal" syntax +``` + +![](object-user-empty.png) + +Usually, the figure brackets `{...}` are used. That declaration is called an *object literal*. + +## Literals and properties + +We can immediately put some properties into `{...}` as "key: value" pairs: + +```js +let user = { // an object + name: "John", // by key "name" store value "John" + age: 30 // by key "age" store value 30 +}; +``` + +A property has a key (also known as "name" or "identifier") before the colon `":"` and a value to the right of it. + +In the `user` object, there are two properties: + +1. The first property has the name `"name"` and the value `"John"`. +2. The second one has the name `"age"` and the value `30`. + +The resulting `user` object can be imagined as a cabinet with two signed files labelled "name" and "age". + +![user object](object-user.png) + +We can add, remove and read files from it any time. + +Property values are accessible using the dot notation: + +```js +// get fields of the object: +alert( user.name ); // John +alert( user.age ); // 30 +``` + +The value can be of any type. Let's add a boolean one: + +```js +user.isAdmin = true; +``` + +![user object 2](object-user-isadmin.png) + +To remove a property, we can use `delete` operator: + +```js +delete user.age; +``` + +![user object 3](object-user-delete.png) + +We can also use multiword property names, but then they must be quoted: + +```js +let user = { + name: "John", + age: 30, + "likes birds": true // multiword property name must be quoted +}; +``` + +![](object-user-props.png) + + +````smart header="Trailing comma" +The last property in the list may end with a comma: +```js +let user = { + name: "John", + age: 30*!*,*/!* +} +``` +That is called a "trailing" or "hanging" comma. Makes it easier to add/remove/move around properties, because all lines become alike. +```` + +## Square brackets + +For multiword properties, the dot access doesn't work: + +```js run +// this would give a syntax error +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. + +There's an alternative "square bracket notation" that works with any string: + + +```js run +let user = {}; + +// set +user["likes birds"] = true; + +// get +alert(user["likes birds"]); // true + +// delete +delete user["likes birds"]; +``` + +Now everything is fine. Please note that the string inside the brackets is properly quoted (any type of quotes will do). + +Square brackets also provide a way to obtain the property name as the result of any expression - as opposed to a litteral string - like from a variable as follows: + +```js +let key = "likes birds"; + +// same as user["likes birds"] = true; +user[key] = true; +``` + +Here, the variable `key` may be calculated at run-time or depend on the user input. And then we use it to access the property. That gives us a great deal of flexibility. The dot notation cannot be used in a similar way. + +For instance: + +```js run +let user = { + name: "John", + age: 30 +}; + +let key = prompt("What do you want to know about the user?", "name"); + +// access by variable +alert( user[key] ); // John (if enter "name") +``` + + +### Computed properties + +We can use square brackets in an object literal. That's called *computed properties*. + +For instance: + +```js run +let fruit = prompt("Which fruit to buy?", "apple"); + +let bag = { +*!* + [fruit]: 5, // the name of the property is taken from the variable fruit +*/!* +}; + +alert( bag.apple ); // 5 if fruit="apple" +``` + +The meaning of a computed property is simple: `[fruit]` means that the property name should be taken from `fruit`. + +So, if a visitor enters `"apple"`, `bag` will become `{apple: 5}`. + +Essentially, that works the same as: +```js run +let fruit = prompt("Which fruit to buy?", "apple"); +let bag = {}; + +// take property name from the fruit variable +bag[fruit] = 5; +``` + +...But looks nicer. + +We can use more complex expressions inside square brackets: + +```js +let fruit = 'apple'; +let bag = { + ['apple' + 'Computers']: 5 // bag.appleComputers = 5 +}; +``` + +Square brackets are much more powerful than the dot notation. They allow any property names and variables. But they are also more cumbersome to write. + +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. If we want to store *arbitrary* (user-provided) keys, then such behavior can be the source of bugs and even vulnerabilities, because it's unexpected. There's another data structure [Map](info:map-set-weakmap-weakset), that we'll learn in the chapter , which supports arbitrary keys. +```` + + +## Property value shorthand + +In real code we often use existing variables as values for property names. + +For instance: + +```js run +function makeUser() { + let name = prompt("Name?"); + let age = prompt("Age?"); + return { + name: name, + age: age + }; +} + +let user = makeUser("John", 30); +alert(user.name); // John +``` + +In the example above, properties have same names as variables. The use-case of making a property from a variable is so common, that there's a special *property value shorthand* to make it shorter. + +Instead of `name:name` we can just write `name`, like this: + +```js +function makeUser(name, age) { +*!* + return { + name, // same as name: name + age // same as age: age + }; +*/!* +} +``` + +We can use both normal properties and shorthands in the same object: + +```js +let user = { + name, // same as name:name + age: 30 +}; +``` + +## 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: + +```js run +let user = {}; + +alert( user.noSuchProperty === undefined ); // true means "no such property" +``` + +There also exists a special operator `"in"` to check for the existance of a property. + +The syntax is: +```js +"key" in object +``` + +For instance: + +```js run +let user = { name: "John", age: 30 }; + +alert( "age" in user ); // true, user.age exists +alert( "blabla" in user ); // false, user.blabla doesn't exist +``` + +Please note that on the left side of `in` there must be a *property name*. That's usually a quoted string. + +If we omit quotes, that would mean a variable containing the actual name to be tested. For instance: + +```js run +let user = { age: 30 }; + +let key = "age"; +alert( *!*key*/!* in user ); // true, takes the name from key and checks for such property +``` + +````smart header="Using \"in\" for properties that store `undefined`" +Usually, the strict comparison `"=== undefined"` check works fine. But there's a special case when it fails, but `"in"` works correctly. + +It's when an object property exists, but stores `undefined`: + +```js run +let obj = { + test: undefined +}; + +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. +```` + + +## The "for..in" loop + +To walk over all keys of an object, there exists a special form of the loop: `for..in`. This is a completely different thing from the `for(;;)` construct that we studied before. + +The syntax: + +```js +for(key in object) { + // executes the body for each key among object properties +} +``` + +For instance, let's output all properties of `user`: + +```js run +let user = { + name: "John", + age: 30, + isAdmin: true +}; + +for(let key in user) { + // keys + alert( key ); // name, age, isAdmin + // values for the keys + alert( user[key] ); // John, 30, true +} +``` + +Note that all "for" constructs allow to declare the looping variable inside the loop, like `let key` here. + +Also, we could use another variable name here instead of `key`. For instance, `"for(let prop in obj)"` is also widely used. + + +### 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 that they are added in it? Can we rely on it? + +The short answer is: "ordered in a special fashion": integer properties are sorted, others appear in creation order. The details follow. + +As an example, let's consider an object with the phone codes: + +```js run +let codes = { + "49": "Germany", + "41": "Switzerland", + "44": "Great Britain", + // .., + "1": "USA" +}; + +*!* +for(let code in codes) { + alert(code); // 1, 41, 44, 49 +} +*/!* +``` + +The object may be used to suggest a list of options to the user. If we're making a site mainly for German audience then we probably want `49` to be the first. + +But if we run the code, we see a totally different picture: + +- USA (1) goes first +- then Switzerland (41) and so on. + +The phone codes go in the ascending sorted order, because they are integer. So we see `1, 41, 44, 49`. + +````smart header="Integer properties? What's that?" +The "integer property" term here means a string that can be converted to-and-from integer without a change. + +So, "49" is an integer property name, because when it's transformed to an integer number and back, it's still the same. But "+49" and "1.2" are not: + +```js run +// Math.trunc is a built-in function that removes the decimal part +alert( String(Math.trunc(Number("49"))) ); // "49", same, integer property +alert( String(Math.trunc(Number("+49"))) ); // "49", not same ⇒ not integer property +alert( String(Math.trunc(Number("1.2"))) ); // "1", not same ⇒ not integer property +``` +```` + +...On the other hand, if the keys are non-integer, then they are listed in the creation order, for instance: + +```js run +let user = { + name: "John", + surname: "Smith" +}; +user.age = 25; // add one more + +*!* +// non-integer properties are listed in the creation order +*/!* +for (let prop in user) { + alert( prop ); // name, surname, age +} +``` + +So, to fix the issue with the phone codes, we can "cheat" by making the codes non-integer. Adding a plus `"+"` sign before each code is enough. + +Like this: + +```js run +let codes = { + "+49": "Germany", + "+41": "Switzerland", + "+44": "Great Britain", + // .., + "+1": "USA" +}; + +for(let code in codes) { + alert( +code ); // 49, 41, 44, 1 +} +``` + +Now it works as intended. + +## Copying by reference + +One of 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 independant variables, each one is storing the string `"Hello!"`. + +![](variable-copy-value.png) + +Objects are not like that. + +**A variable stores not the object itself, but it's "address in memory", in other words "a reference" to it.** + +Here's the picture for the object: + +```js +let user = { + name: "John" +}; +``` + +![](variable-contains-reference.png) + +Here, the object is stored somewhere in memory. And the variable `user` has a "reference" to it. + +**When an object variable is copied -- the reference is copied, the object is not duplicated.** + +If we imagine an object as a cabinet, then a variable is a key to it. Copying a variable duplicates the key, but not the cabinet itself. + +For instance: + +```js no-beautify +let user = { name: "John" }; + +let admin = user; // copy the reference +``` + +Now we have two variables, each one with the reference to the same object: + +![](variable-copy-reference.png) + +Now 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. Like 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 see changes. + +### Comparison by reference + +The equality `==` and strict equality `===` operators for objects work exactly the same. + +**Two objects are equal only if they are the same object.** + +For instance, two variables reference the same object, they are equal: + +```js run +let a = {}; +let b = a; // copy the reference + +alert( a == b ); // true, both variables reference the same object +alert( a === b ); // true +``` + +And here two independent objects are not equal, even though both are empty: + +```js run +let a = {}; +let b = {}; // two independent objects + +alert( a == b ); // false +``` + +For comparisons like `obj1 > obj2` or for a comparison against a primitive `obj == 5`, objects are converted to primitives. We'll study how object conversions work very soon, but to say the truth, such comparisons are necessary very rarely and usually are a result of a coding mistake. + +### Const object + +An object declared as `const` *can* be changed. + +For instance: + +```js run +const user = { + name: "John" +}; + +*!* +user.age = 25; // (*) +*/!* + +alert(user.age); // 25 +``` + +It might seem that the line `(*)` would cause an error, but no, there's totally no problem. That's because `const` fixes the value of `user` itself. And here `user` stores the reference to the same object all the time. The line `(*)` goes *inside* the object, it doesn't reassign `user`. + +The `const` would give an error if we try to set `user` to something else, for instance: + +```js run +const user = { + name: "John" +}; + +*!* +// Error (can't reassign user) +*/!* +user = { + name: "Pete" +}; +``` + +...But what if we want to make constant object properties? So that `user.age = 25` would give an error. That's possible too. We'll cover it in the chapter . + +## Cloning and merging, Object.assign + +So, copying an object variable creates one more reference to the same object. + +But what if we need to duplicate an object? Create an independant copy, a clone? + +That's also doable, but a little bit more difficult, because there's no built-in method for that in JavaScript. Actually, that's rarely needed, copying by reference is good most of the time. + +But if we really want that, then we need to create a new object and replicate the structure of the existing one by iterating over its properties and copying them on the primitive level. + +Like this: + +```js run +let user = { + name: "John", + age: 30 +}; + +*!* +let clone = {}; // the new empty object + +// let's copy all user properties into it +for (let key in user) { + clone[key] = user[key]; +} +*/!* + +// now clone is a fully independant clone +clone.name = "Pete"; // changed the data in it + +alert( user.name ); // still John in the original object +``` + +Also we can use the method [Object.assign](mdn:js/Object/assign) for that. + +The syntax is: + +```js +Object.assign(dest[, src1, src2, src3...]) +``` + +- Arguments `dest`, and `src1, ..., srcN` (can be as many as needed) are objects. +- It copies the properties of all objects `src1, ..., srcN` into `dest`. In other words, properties of all arguments starting from the 2nd are copied into the 1st. Then it returns `dest`. + +For instance, we can use it to merge several objects into one: +```js +let user = { name: "John" }; + +let permissions1 = { canView: true }; +let permissions2 = { canEdit: true }; + +*!* +// copies all properties from permissions1 and permissions2 into user +Object.assign(user, permissions1, permissions2); +*/!* + +// now user = { name: "John", canView: true, canEdit: true } +``` + +If the receiving object (`user`) already has the same named property, it will be overwritten: + +```js +let user = { name: "John" }; + +// overwrite name, add isAdmin +Object.assign(user, { name: "Pete", isAdmin: true }); + +// now user = { name: "Pete", isAdmin: true } +``` + +We also can use `Object.assign` to replace the loop for simple cloning: + +```js +let user = { + name: "John", + age: 30 +}; + +*!* +let clone = Object.assign({}, user); +*/!* +``` + +It copies all properties of `user` into the empty object and returns it. Actually, the same as the loop, but shorter. + +Till 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 it's 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://w3c.github.io/html/infrastructure.html#internal-structured-cloning-algorithm). 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. + +They store properties (key-value pairs), where: +- Property keys must be strings or symbols (usually strings). +- Values can be of any type. + +To access a property, we can use: +- The dot notation: `obj.property`. +- Square brackets notation `obj["property"]`. Square brackets allow to take the key from a variable, like `obj[varWithKey]`. + +Additional operators: +- To delete a property: `delete obj.prop`. +- To check if a property with the given key exists: `"key" in obj`. +- To iterate over an object: `for(let key in obj)` loop. + +Objects are assigned and copied by reference. In other words, a variable stores not the "object value", but a "reference" (address in memory) for the value. So copying such a variable or passing it as a function argument copies that reference, not the object. All operations via copied references (like adding/removed 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: + +- `Array` to store ordered data collections, +- `Date` to store the information about the date and time, +- `Error` to store the information about an error. +- ...And so on. + +They have their special features that we'll study later. Sometimes people say something like "Array type" or "Date type", but formally they are not types of their own, but belong to a single "object" data type. And they extend it in various ways. + +Objects in JavaScript are very powerful. Here we've just scratched the surface of the topic that is really huge. We'll be closely working with objects and learning more about them in further parts of the tutorial. diff --git a/1-js/04-object-basics/01-object/object-user-delete.png b/1-js/04-object-basics/01-object/object-user-delete.png new file mode 100644 index 00000000..688158f9 Binary files /dev/null and b/1-js/04-object-basics/01-object/object-user-delete.png differ diff --git a/1-js/04-object-basics/01-object/object-user-delete@2x.png b/1-js/04-object-basics/01-object/object-user-delete@2x.png new file mode 100644 index 00000000..e1ef6554 Binary files /dev/null and b/1-js/04-object-basics/01-object/object-user-delete@2x.png differ diff --git a/1-js/04-object-basics/01-object/object-user-empty.png b/1-js/04-object-basics/01-object/object-user-empty.png new file mode 100644 index 00000000..483d072c Binary files /dev/null and b/1-js/04-object-basics/01-object/object-user-empty.png differ diff --git a/1-js/04-object-basics/01-object/object-user-empty@2x.png b/1-js/04-object-basics/01-object/object-user-empty@2x.png new file mode 100644 index 00000000..8db894cb Binary files /dev/null and b/1-js/04-object-basics/01-object/object-user-empty@2x.png differ diff --git a/1-js/04-object-basics/01-object/object-user-isadmin.png b/1-js/04-object-basics/01-object/object-user-isadmin.png new file mode 100644 index 00000000..4e76eeb7 Binary files /dev/null and b/1-js/04-object-basics/01-object/object-user-isadmin.png differ diff --git a/1-js/04-object-basics/01-object/object-user-isadmin@2x.png b/1-js/04-object-basics/01-object/object-user-isadmin@2x.png new file mode 100644 index 00000000..b4097769 Binary files /dev/null and b/1-js/04-object-basics/01-object/object-user-isadmin@2x.png differ diff --git a/1-js/04-object-basics/01-object/object-user-props.png b/1-js/04-object-basics/01-object/object-user-props.png new file mode 100644 index 00000000..2bfdfabd Binary files /dev/null and b/1-js/04-object-basics/01-object/object-user-props.png differ diff --git a/1-js/04-object-basics/01-object/object-user-props@2x.png b/1-js/04-object-basics/01-object/object-user-props@2x.png new file mode 100644 index 00000000..4935b59c Binary files /dev/null and b/1-js/04-object-basics/01-object/object-user-props@2x.png differ diff --git a/1-js/04-object-basics/01-object/object-user.png b/1-js/04-object-basics/01-object/object-user.png new file mode 100644 index 00000000..16179209 Binary files /dev/null and b/1-js/04-object-basics/01-object/object-user.png differ diff --git a/1-js/04-object-basics/01-object/object-user@2x.png b/1-js/04-object-basics/01-object/object-user@2x.png new file mode 100644 index 00000000..72038953 Binary files /dev/null and b/1-js/04-object-basics/01-object/object-user@2x.png differ diff --git a/1-js/04-object-basics/01-object/object.png b/1-js/04-object-basics/01-object/object.png new file mode 100644 index 00000000..f94d094a Binary files /dev/null and b/1-js/04-object-basics/01-object/object.png differ diff --git a/1-js/04-object-basics/01-object/object@2x.png b/1-js/04-object-basics/01-object/object@2x.png new file mode 100644 index 00000000..003c2f6e Binary files /dev/null and b/1-js/04-object-basics/01-object/object@2x.png differ diff --git a/1-js/04-object-basics/01-object/variable-contains-reference.png b/1-js/04-object-basics/01-object/variable-contains-reference.png new file mode 100644 index 00000000..d6e7fddf Binary files /dev/null and b/1-js/04-object-basics/01-object/variable-contains-reference.png differ diff --git a/1-js/04-object-basics/01-object/variable-contains-reference@2x.png b/1-js/04-object-basics/01-object/variable-contains-reference@2x.png new file mode 100644 index 00000000..145bad29 Binary files /dev/null and b/1-js/04-object-basics/01-object/variable-contains-reference@2x.png differ diff --git a/1-js/04-object-basics/01-object/variable-copy-reference.png b/1-js/04-object-basics/01-object/variable-copy-reference.png new file mode 100644 index 00000000..97510c4b Binary files /dev/null and b/1-js/04-object-basics/01-object/variable-copy-reference.png differ diff --git a/1-js/04-object-basics/01-object/variable-copy-reference@2x.png b/1-js/04-object-basics/01-object/variable-copy-reference@2x.png new file mode 100644 index 00000000..a64238a5 Binary files /dev/null and b/1-js/04-object-basics/01-object/variable-copy-reference@2x.png differ diff --git a/1-js/04-object-basics/01-object/variable-copy-value.png b/1-js/04-object-basics/01-object/variable-copy-value.png new file mode 100644 index 00000000..e21af099 Binary files /dev/null and b/1-js/04-object-basics/01-object/variable-copy-value.png differ diff --git a/1-js/04-object-basics/01-object/variable-copy-value@2x.png b/1-js/04-object-basics/01-object/variable-copy-value@2x.png new file mode 100644 index 00000000..2f0b2f47 Binary files /dev/null and b/1-js/04-object-basics/01-object/variable-copy-value@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/article.md b/1-js/04-object-basics/02-garbage-collection/article.md new file mode 100644 index 00000000..d8a6eee8 --- /dev/null +++ b/1-js/04-object-basics/02-garbage-collection/article.md @@ -0,0 +1,216 @@ +# Garbage collection + +Memory management in JavaScript is performed automatically and invisibly to us. We create primitives, objects, functions... All that takes memory. + +What happens when something is not needed any more? How JavaScript engine discovers that and cleans up? + +[cut] + +## Reachability + +The main concept of memory management in JavaScript is *reachability*. + +Simply put, "reachable" values are those that are accessible or useable somehow. They are guaranteed to be stored in memory. + +1. There's a base set of inherently reachable values, that cannot be deleted for obvious reasons. + + For instance: + + - Local variables and parameters of the current function. + - Variables and parameters for other functions on the current chain of nested calls. + - Global variables. + - (there are some other, internal ones as well) + + These values are called *roots*. + +2. Any other value is considered reachable if it's reachable from a root by a reference of by a chain of references. + + For instance, if there's an object in a local variable, and that object has a property referencing another object, that object is considered reachable. And those that it references -- are also reachable. Detailed examples to follow. + +There's a background process in the JavaScript engine that is called [garbage collector](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)). It monitors all objects and removes those that became unreachable. + +## A simple example + +Here's the simplest example: + +```js +// user has a reference to the object +let user = { + name: "John" +}; +``` + +![](memory-user-john.png) + +Here the arrow depicts an object reference. The global variable `"user"` references the object `{name: "John"}` (we'll call it John for brevity). The `"name"` property of John stores a primitive, so it's painted inside the object. + +If the value of `user` is overwritten, the reference is lost: + +```js +user = null; +``` + +![](memory-user-john-lost.png) + +Now John becomes unreachable. There's no way to access it, no references to it. Garbage collector will junk the data and free the memory. + +## Two references + +Now let's imagine we copied the reference from `user` to `admin`: + +```js +// user has a reference to the object +let user = { + name: "John" +}; + +*!* +let admin = user; +*/!* +``` + +![](memory-user-john-admin.png) + +Now if we do the same: +```js +user = null; +``` + +...Then the object is still reachable via `admin` global variable, so it's in memory. If we overwrite `admin` too, then it can be removed. + +## Interlinked objects + +Now a more complex example. The family: + +```js +function marry(man, woman) { + woman.husband = man; + man.wife = woman; + + return { + father: man, + mother: woman + } +} + +let family = marry({ + name: "John" +}, { + name: "Ann" +}); +``` + +Function `marry` "marries" two objects by giving them references to each other and returns a new object that contains them both. + +The resulting memory structure: + +![](family.png) + +As of now, all objects are reachable. + +Now let's remove two references: + +```js +delete family.father; +delete family.mother.husband; +``` + +![](family-delete-refs.png) + +It's not enough to delete only one of these two references, because all objects would still be reachable. + +But if we delete both, then we can see that John has no incoming reference any more: + +![](family-no-father.png) + +Outgoing references do not matter. Only incoming ones can make an object reachable. So, John is now unreachable and will be removed from the memory with all its data that also became unaccessible. + +After garbage collection: + +![](family-no-father-2.png) + +## Unreachable island + +It is possible that the whole island of interlinked objects becomes unreachable and is removed from the memory. + +The source object is the same as above. Then: + +```js +family = null; +``` + +The in-memory picture becomes: + +![](family-no-family.png) + +This example demonstrates how important the concept of reachability is. + +It's obvious that John and Ann are still linked, both have incoming references. But that's not enough. + +The former `"family"` object has been unlinked from the root, there's no reference to it any more, so the whole island becomes unreachable and will be removed. + +## Internal algorithms + +The basic garbage collection algorithm is called "mark-and-sweep". + +Regularly the following "garbage collection" steps are performed: + +- The garbage collector takes roots and "marks" (remembers) them. +- Then it visits and "marks" all references from them. +- Then it visits marked objects and marks *their* references. All visited objects are remembered, not to visit the same object twice in the future. +- ...And so on until there are unvisited references (reachable from the roots). +- All objects except marked ones are removed. + +For instance, let our object structure look like this: + +![](garbage-collection-1.png) + +We can clearly see an "unreachable island" to the right side. Now let's see how "mark-and-sweep" garbage collector deals with it. + +The first step marks the roots: + +![](garbage-collection-2.png) + +Then their references are marked: + +![](garbage-collection-3.png) + +...And their refences, while possible: + +![](garbage-collection-4.png) + +Now the objects that could not be visited in the process are considered unreachable and will be removed: + +![](garbage-collection-5.png) + +That's the concept how garbage collection works. + +JavaScript engines apply many optimizations to make it run faster and not affect the execution. + +Some of the optimizations: + +- **Generational collection** -- objects are split into two sets: "new ones" and "old ones". Many objects appear, then do their job and die fast, so they can be cleaned up aggressively. Those that survive for long enough, become "old". +- **Incremental collection** -- if there are many objects, and we try to walk and mark the whole object set at once, it may take some time and introduce visible delays in the execution. So the engine tries to split the job into pieces. Then pieces are executed one at a time. That requires some extra bookkeeping between them to track changes. +- **Idle-time collection** -- the garbage collector tries to run only while the CPU is idle, to reduce the possible effect on the execution. + +There are other optimizations and flavours of garbage collection algorithms. As much as I'd like to describe them here, I have to hold off, because different engines implement different tweaks and techniques. + +And -- what's even more important, things change as engines develop, so going really deep "in advance", without a real need is probably not worth that. Unless, of course, it is a matter of pure interest, then there will be some links for you below. + +## Summary + +The main things to know: + +- Garbage collection is performed automatically. We cannot force or prevent it. +- Objects are retained in memory while they are reachable. +- Being referenced is not the same as being reachable (from a root): a pack of interlinked objects can become unreachable as a whole. + +Modern engines implement advanced algorithms of garbage collection. + +A general book "The Garbage Collection Handbook: The Art of Automatic Memory Management" (R. Jones at al) covers some of them. + +If you are familiar with low-level programming, the more detailed information about V8 garbage collector is in the article [A tour of V8: Garbage Collection](http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection). + +[V8 blog](http://v8project.blogspot.com/) also publishes articles about changes in memory management from time to time. Naturally, to learn the garbage collection, you'd better prepare by learning about V8 internals in general and read the blog of [Vyacheslav Egorov](http://mrale.ph) who worked as one of V8 engineers. I'm saying: "V8", because it is best covered with articles in the internet. For other engines, many approaches are similar, but garbage collection differs in many aspects. + +In-depth knowledge of engines is good when you need low-level optimizations. It would be wise to plan that as the next step after you're familiar with the language. diff --git a/1-js/04-object-basics/02-garbage-collection/family-delete-refs.png b/1-js/04-object-basics/02-garbage-collection/family-delete-refs.png new file mode 100644 index 00000000..5c10f0e4 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/family-delete-refs.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-delete-refs@2x.png b/1-js/04-object-basics/02-garbage-collection/family-delete-refs@2x.png new file mode 100644 index 00000000..24d29630 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/family-delete-refs@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-family.png b/1-js/04-object-basics/02-garbage-collection/family-no-family.png new file mode 100644 index 00000000..a4ce30a3 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/family-no-family.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-family@2x.png b/1-js/04-object-basics/02-garbage-collection/family-no-family@2x.png new file mode 100644 index 00000000..0d994983 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/family-no-family@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father-2.png b/1-js/04-object-basics/02-garbage-collection/family-no-father-2.png new file mode 100644 index 00000000..e24dba5b Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/family-no-father-2.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father-2@2x.png b/1-js/04-object-basics/02-garbage-collection/family-no-father-2@2x.png new file mode 100644 index 00000000..a6c4ee36 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/family-no-father-2@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father.png b/1-js/04-object-basics/02-garbage-collection/family-no-father.png new file mode 100644 index 00000000..df14624b Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/family-no-father.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father@2x.png b/1-js/04-object-basics/02-garbage-collection/family-no-father@2x.png new file mode 100644 index 00000000..5ab4b379 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/family-no-father@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/family.png b/1-js/04-object-basics/02-garbage-collection/family.png new file mode 100644 index 00000000..dbbc01d2 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/family.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/family@2x.png b/1-js/04-object-basics/02-garbage-collection/family@2x.png new file mode 100644 index 00000000..64b4619b Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/family@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.png new file mode 100644 index 00000000..42319177 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-1@2x.png new file mode 100644 index 00000000..223ea32a Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/garbage-collection-1@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.png new file mode 100644 index 00000000..da63d396 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-2@2x.png new file mode 100644 index 00000000..1f614e3e Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/garbage-collection-2@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.png new file mode 100644 index 00000000..e77144c1 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-3@2x.png new file mode 100644 index 00000000..37e349b6 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/garbage-collection-3@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.png new file mode 100644 index 00000000..110e0d9c Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-4@2x.png new file mode 100644 index 00000000..c09d75f9 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/garbage-collection-4@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.png new file mode 100644 index 00000000..bc4ea967 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-5@2x.png new file mode 100644 index 00000000..0ab697e6 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/garbage-collection-5@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection.png new file mode 100644 index 00000000..013d0a48 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/garbage-collection.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection@2x.png new file mode 100644 index 00000000..681f29e4 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/garbage-collection@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.png new file mode 100644 index 00000000..29c4fcbe Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin@2x.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin@2x.png new file mode 100644 index 00000000..2f80f19a Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.png new file mode 100644 index 00000000..cdc1d490 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost@2x.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost@2x.png new file mode 100644 index 00000000..d58afdb5 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost@2x.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john.png new file mode 100644 index 00000000..3ba5730d Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/memory-user-john.png differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john@2x.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john@2x.png new file mode 100644 index 00000000..5aa81bb0 Binary files /dev/null and b/1-js/04-object-basics/02-garbage-collection/memory-user-john@2x.png differ diff --git a/1-js/04-object-basics/03-symbol/article.md b/1-js/04-object-basics/03-symbol/article.md new file mode 100644 index 00000000..4509b60b --- /dev/null +++ b/1-js/04-object-basics/03-symbol/article.md @@ -0,0 +1,236 @@ + +# Symbol type + +By specification, object property keys may be either of string type, or of symbol type. Not numbers, not booleans, only strings or symbols, these two types. + +Till now we only saw strings. Now let's see the advantages that symbols can give us. + +[cut] + +## Symbols + +"Symbol" value represents an unique identifier with a given name. + +A value of this type can be created using `Symbol(name)`: + +```js +// id is a symbol with the name "id" +let id = Symbol("id"); +``` + +Symbols are guaranteed to be unique. Even if we create many symbols with the same name, they are different values. + +For instance, here are two symbols with the same name -- they are not equal: + +```js run +let id1 = Symbol("id"); +let id2 = Symbol("id"); + +*!* +alert(id1 == id2); // false +*/!* +``` + +If you are familiar with Ruby or another language that also has some sort of "symbols" -- please don't be misguided. JavaScript symbols are different. + + +## "Hidden" properties + +Symbols allow to create "hidden" properties of an object, that no other part of code can occasionally access or overwrite. + +For instance, if we want to store an "identifier" for the object `user`, we can create a symbol with the name `id` for it: + +```js run +let user = { name: "John" }; +let id = Symbol("id"); + +user[id] = "ID Value"; +alert( user[id] ); // we can access the data using the symbol as the key +``` + +Now let's imagine that another script wants to have his own "id" property inside `user`, for his own purposes. That may be another JavaScript library, so the scripts are completely unaware for each other. + +No problem. It can create its own `Symbol("id")`. + +Their script: + +```js +// ... +let id = Symbol("id"); + +user[id] = "Their id value"; +``` + +There will be no conflict, because symbols are always different, even if they have the same name. + +Please note that if we used a string `"id"` instead of a symbol for the same purpose, then there *would* be a conflict: + +```js run +let user = { name: "John" }; + +// our script uses "id" property +user.id = "ID Value"; + +// ...if later another script the uses "id" for its purposes... + +user.id = "Their id value" +// boom! overwritten! it did not mean to harm the colleague, but did it! +``` + +### Symbols in a literal + +If we want to use a symbol in an object literal, we need square brackets. + +Like this: + +```js +let id = Symbol("id"); + +let user = { + name: "John", +*!* + [id]: 123 // not just "id: 123" +*/!* +}; +``` +That's because we need the value from the variable `id` as the key, not the string "id". + +### Symbols skipped by for..in + +Symbolic properties do not participate in `for..in` loop. + +For instance: + +```js run +let id = Symbol("id"); +let user = { + name: "John", + age: 30, + [id]: 123 +}; + +*!* +for(let key in user) alert(key); // name, age (no symbols) +*/!* + +// the direct access by the symbol works +alert( "Direct: " + user[id] ); +``` + +That's a part of the general "hiding" concept. If another script or a library loops over our object, it won't unexpectedly access a symbolic property. + +In contrast, [Object.assign](mdn:js/Object/assign) copies both string and symbol properties: + +```js run +let id = Symbol("id"); +let user = { + [id]: 123 +}; + +let clone = Object.assign({}, user); + +alert( clone[id] ); // 123 +``` + +There's no paradox here. That's by design. The idea is that when we clone an object or merge objects, we usually want symbolic properties (like `id`) to be copied as well. + +````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 coerced to strings. + +For instance: + +```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 + +Normally, all symbols are different. But sometimes we want same-named symbols to be the same. + +For instance, different parts of our application want to access symbol `"id"` meaning exactly the same property. + +To achieve that, there exists a *global symbol registry*. We can create symbols in it and and access them later, and it guarantees that repeated accesses by the same name return exactly the same symbol. + +In order to create or read a symbol in the registry, use `Symbol.for(name)`. + +For instance: + +```js run +// read from the global registry +let name = Symbol.for("name"); // if the symbol did not exist, it is created + +// read it again +let nameAgain = Symbol.for("name"); + +// the same symbol +alert( name === nameAgain ); // true +``` + +Symbols inside the registry are called *global symbols*. If we want an application-wide symbol, accessible everywhere in the code -- that's what they are for. + +```smart header="That sounds like Ruby" +In some programming languages, like Ruby, there's a single symbol per name. + +In JavaScript, as we can see, that's right for global symbols. +``` + +### Symbol.keyFor + +For global symbols, not only `Symbol.for(name)` returns a symbol by name, but there's a reverse call: `Symbol.keyFor(sym)`, that does the reverse: returns a name by a global symbol. + +For instance: + +```js run +let sym = Symbol.for("name"); +let sym2 = Symbol.for("id"); + +// get name from symbol +alert( Symbol.keyFor(sym) ); // name +alert( Symbol.keyFor(sym2) ); // id +``` + +The `Symbol.keyFor` internally uses the global symbol registry to look up the name for the symbol. So it doesn't work for non-global symbols. If the symbol is not global, it won't be able to find it and return `undefined`. + +For instance: + +```js run +alert( Symbol.keyFor(Symbol.for("name")) ); // name, global symbol + +alert( Symbol.keyFor(Symbol("name2")) ); // undefined, non-global symbol +``` + +For non-global symbols, the name is only used for debugging purposes. + +## System symbols + +There exist many "system" symbols that JavaScript uses internally, and we can use them to fine-tune various aspects of our objects. + +They are listed in the specification in the [Well-known symbols](https://tc39.github.io/ecma262/#sec-well-known-symbols) table: + +- `Symbol.hasInstance` +- `Symbol.isConcatSpreadable` +- `Symbol.iterator` +- `Symbol.toPrimitive` +- ...and so on. + +For instance, `Symbol.toPrimitive` allows to describe object to primitive conversion. We'll see its use very soon. + +Other symbols will also become familiar when we study the corresponding language features. + +## Summary + +- Symbol is a primitive type for unique identifiers. +- Symbols are created with `Symbol(name)` call. +- Symbols are useful if we want to create a field that only those who know the symbol can access. +- Symbols don't appear in `for..in` loops. +- Symbols created with `Symbol(name)` are always different, even if they have the same name. If we want same-named symbols to be equal, then we should use the global registry: `Symbol.for(name)` returns (creates if needed) a global symbol with the given name. Multiple calls return the same symbol. +- There are system symbols used by JavaScript and accessible as `Symbol.*`. We can use them to alter some built-in behaviors. + +Technically, symbols are not 100% hidden. There is a build-in method [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols) that allows to get all symbols. Also there is a method named [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) that returns *all* keys of an object including symbolic ones. So they are not really hidden. But most libraries, built-in methods and syntax constructs adhere to a common agreement that they are. And the one who explicitly calls the aforementioned methods probably understands well what he's doing. diff --git a/1-js/04-object-basics/04-object-methods/2-check-syntax/solution.md b/1-js/04-object-basics/04-object-methods/2-check-syntax/solution.md new file mode 100644 index 00000000..e2e87de7 --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/2-check-syntax/solution.md @@ -0,0 +1,43 @@ +**Error**! + +Try it: + +```js run +let user = { + name: "John", + go: function() { alert(this.name) } +} + +(user.go)() // error! +``` + +The error message in most browsers does not give understanding what went wrong. + +**The error appears because a semicolon is missing after `user = {...}`.** + +JavaScript does not assume a semicolon before a bracket `(user.go)()`, so it reads the code like: + +```js no-beautify +let user = { go:... }(user.go)() +``` + +Then we can also see that such a joint expression is syntactically a call of the object `{ go: ... }` as a function with the argument `(user.go)`. And that also happens on the same line with `let user`, so the `user` object has not yet even been defined, hence the error. + +If we insert the semicolon, all is fine: + +```js run +let user = { + name: "John", + go: function() { alert(this.name) } +}*!*;*/!* + +(user.go)() // John +``` + +Please note that brackets around `(user.go)` do nothing here. Usually they setup the order of operations, but here the dot `.` works first anyway, so there's no effect. Only the semicolon thing matters. + + + + + + diff --git a/1-js/04-object-basics/04-object-methods/2-check-syntax/task.md b/1-js/04-object-basics/04-object-methods/2-check-syntax/task.md new file mode 100644 index 00000000..bb4d5b86 --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/2-check-syntax/task.md @@ -0,0 +1,19 @@ +importance: 2 + +--- + +# Syntax check + +What is the resule of this code? + + +```js no-beautify +let user = { + name: "John", + go: function() { alert(this.name) } +} + +(user.go)() +``` + +P.S. There's a pitfall :) \ No newline at end of file diff --git a/1-js/04-object-basics/04-object-methods/3-why-this/solution.md b/1-js/04-object-basics/04-object-methods/3-why-this/solution.md new file mode 100644 index 00000000..89bc0d72 --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/3-why-this/solution.md @@ -0,0 +1,22 @@ + +Here's the explanations. + +1. That's a regular object method call. + +2. The same, brackets do not change the order of operations here, the dot is first anyway. + +3. Here we have a more complex call `(expression).method()`. The call works as if it were split into two lines: + + ```js no-beautify + f = obj.go; // calculate the expression + f(); // call what we have + ``` + + Here `f()` is executed as a function, without `this`. + +4. The similar thing as `(3)`, to the left of the dot `.` we have an expression. + +To explain the behavior of `(3)` and `(4)` we need to recall that property accessors (dot or square brackets) return a value of the Reference Type. + +Any operation on it except a method call (like assignment `=` or `||`) turns it into an ordinary value, which does not carry the information allowing to set `this`. + diff --git a/1-js/04-object-basics/04-object-methods/3-why-this/task.md b/1-js/04-object-basics/04-object-methods/3-why-this/task.md new file mode 100644 index 00000000..2b6e8cd1 --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/3-why-this/task.md @@ -0,0 +1,26 @@ +importance: 3 + +--- + +# Explain the value of "this" + +In the code above we intend to call `user.go()` method 4 times in a row. + +But calls `(1)` and `(2)` works differently from `(3)` and `(4)`. Why? + +```js run no-beautify +let obj, method; + +obj = { + go: function() { alert(this); } +}; + +obj.go(); // (1) [object Object] + +(obj.go)(); // (2) [object Object] + +(method = obj.go)(); // (3) undefined + +(obj.go || obj.stop)(); // (4) undefined +``` + diff --git a/1-js/04-object-basics/04-object-methods/4-object-property-this/solution.md b/1-js/04-object-basics/04-object-methods/4-object-property-this/solution.md new file mode 100644 index 00000000..f5773ec2 --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/4-object-property-this/solution.md @@ -0,0 +1,46 @@ +**Answer: an error.** + +Try it: +```js run +function makeUser() { + return { + name: "John", + ref: this + }; +}; + +let user = makeUser(); + +alert( user.ref.name ); // Error: Cannot read property 'name' of undefined +``` + +That's because rules that set `this` do not look at object literals. + +Here the value of `this` inside `makeUser()` is `undefined`, because it is called as a function, not as a method. + +And the object literal itself has no effect on `this`. The value of `this` is one for the whole function, code blocks and object literals do not affect it. + +So `ref: this` actually takes current `this` of the function. + +Here's the opposite case: + +```js run +function makeUser() { + return { + name: "John", +*!* + ref() { + return this; + } +*/!* + }; +}; + +let user = makeUser(); + +alert( user.ref().name ); // John +``` + +Now it works, because `user.ref()` is a method. And the value of `this` is set to the object before dot `.`. + + diff --git a/1-js/04-object-basics/04-object-methods/4-object-property-this/task.md b/1-js/04-object-basics/04-object-methods/4-object-property-this/task.md new file mode 100644 index 00000000..4784b082 --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/4-object-property-this/task.md @@ -0,0 +1,23 @@ +importance: 5 + +--- + +# Using "this" in object literal + +Here the function `makeUser` returns an object. + +What is the result of accessing its `ref`? Why? + +```js +function makeUser() { + return { + name: "John", + ref: this + }; +}; + +let user = makeUser(); + +alert( user.ref.name ); // What's the result? +``` + diff --git a/1-js/04-object-basics/04-object-methods/7-calculator/_js.view/solution.js b/1-js/04-object-basics/04-object-methods/7-calculator/_js.view/solution.js new file mode 100644 index 00000000..9ccbe43a --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/7-calculator/_js.view/solution.js @@ -0,0 +1,14 @@ +let calculator = { + sum() { + return this.a + this.b; + }, + + mul() { + return this.a * this.b; + }, + + read() { + this.a = +prompt('a?', 0); + this.b = +prompt('b?', 0); + } +}; \ No newline at end of file diff --git a/1-js/04-object-basics/04-object-methods/7-calculator/_js.view/test.js b/1-js/04-object-basics/04-object-methods/7-calculator/_js.view/test.js new file mode 100644 index 00000000..1f71eda4 --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/7-calculator/_js.view/test.js @@ -0,0 +1,28 @@ + + +describe("calculator", function() { + + context("when 2 and 3 entered", function() { + beforeEach(function() { + sinon.stub(window, "prompt"); + + prompt.onCall(0).returns("2"); + prompt.onCall(1).returns("3"); + + calculator.read(); + }); + + afterEach(function() { + prompt.restore(); + }); + + it("the sum is 5", function() { + assert.equal(calculator.sum(), 5); + }); + + it("the multiplication product is 6", function() { + assert.equal(calculator.mul(), 6); + }); + }); + +}); diff --git a/1-js/04-object-basics/04-object-methods/7-calculator/solution.md b/1-js/04-object-basics/04-object-methods/7-calculator/solution.md new file mode 100644 index 00000000..22c4bf18 --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/7-calculator/solution.md @@ -0,0 +1,23 @@ + + +```js run demo +let calculator = { + sum() { + return this.a + this.b; + }, + + mul() { + return this.a * this.b; + }, + + read() { + this.a = +prompt('a?', 0); + this.b = +prompt('b?', 0); + } +}; + +calculator.read(); +alert( calculator.sum() ); +alert( calculator.mul() ); +``` + diff --git a/1-js/04-object-basics/04-object-methods/7-calculator/task.md b/1-js/04-object-basics/04-object-methods/7-calculator/task.md new file mode 100644 index 00000000..aa22608e --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/7-calculator/task.md @@ -0,0 +1,24 @@ +importance: 5 + +--- + +# Create a calculator + +Create an object `calculator` with three methods: + +- `read()` prompts for two values and saves them as object properties. +- `sum()` returns the sum of saved values. +- `mul()` multiplies saved values and returns the result. + +```js +let calculator = { + // ... your code ... +}; + +calculator.read(); +alert( calculator.sum() ); +alert( calculator.mul() ); +``` + +[demo] + diff --git a/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/solution.js b/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/solution.js new file mode 100644 index 00000000..e98fe641 --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/solution.js @@ -0,0 +1,15 @@ + +let ladder = { + step: 0, + up: function() { + this.step++; + return this; + }, + down: function() { + this.step--; + return this; + }, + showStep: function() { + alert(this.step); + } +}; \ No newline at end of file diff --git a/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/test.js b/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/test.js new file mode 100644 index 00000000..a2b17fcc --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/test.js @@ -0,0 +1,40 @@ + +describe('Ladder', function() { + before(function() { + window.alert = sinon.stub(window, "alert"); + }); + + beforeEach(function() { + ladder.step = 0; + }); + + it('up() should return this', function() { + assert.equal(ladder.up(), ladder); + }); + + it('down() should return this', function() { + assert.equal(ladder.down(), ladder); + }); + + it('showStep() should call alert', function() { + ladder.showStep(); + assert(alert.called); + }); + + it('up() should increase step', function() { + assert.equal(ladder.up().up().step, 2); + }); + + it('down() should decrease step', function() { + assert.equal(ladder.down().step, -1); + }); + + it('down().up().up().up() ', function() { + assert.equal(ladder.down().up().up().up().step, 2); + }); + + after(function() { + ladder.step = 0; + alert.restore(); + }); +}); diff --git a/1-js/04-object-basics/04-object-methods/8-chain-calls/solution.md b/1-js/04-object-basics/04-object-methods/8-chain-calls/solution.md new file mode 100644 index 00000000..41edd723 --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/8-chain-calls/solution.md @@ -0,0 +1,40 @@ +The solution is to return the object itself from every call. + +```js run +let ladder = { + step: 0, + up() { + this.step++; +*!* + return this; +*/!* + }, + down() { + this.step--; +*!* + return this; +*/!* + }, + showStep() { + alert( this.step ); +*!* + return this; +*/!* + } +} + +ladder.up().up().down().up().down().showStep(); // 1 +``` + +We also can write a single call per line. For long chains it's more readable: + +```js +ladder + .up() + .up() + .down() + .up() + .down() + .showStep(); // 1 +``` + diff --git a/1-js/04-object-basics/04-object-methods/8-chain-calls/task.md b/1-js/04-object-basics/04-object-methods/8-chain-calls/task.md new file mode 100644 index 00000000..a989846f --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/8-chain-calls/task.md @@ -0,0 +1,39 @@ +importance: 2 + +--- + +# Chaining + +There's a `ladder` object that allows to go up and down: + +```js +let ladder = { + step: 0, + up() { + this.step++; + }, + down() { + this.step--; + }, + showStep: function() { // shows the current step + alert( this.step ); + } +}; +``` + +Now, if we need to make several calls in sequence, can do it like this: + +```js +ladder.up(); +ladder.up(); +ladder.down(); +ladder.showStep(); // 1 +``` + +Modify the code of `up` and `down` to make the calls chainable, like this: + +```js +ladder.up().up().down().showStep(); // 1 +``` + +Such approach is widely used across JavaScript libraries. diff --git a/1-js/04-object-basics/04-object-methods/article.md b/1-js/04-object-basics/04-object-methods/article.md new file mode 100644 index 00000000..44a867e2 --- /dev/null +++ b/1-js/04-object-basics/04-object-methods/article.md @@ -0,0 +1,355 @@ +# Object methods, "this" + +Objects are usually created to represent entities of the real world, like users, orders and so on: + +```js +let user = { + name: "John", + age: 30 +}; +``` + +And, in the real world, a user can *act*: select something from the shopping cart, login, logout etc. + +Actions are represented in JavaScript by functions in properties. + +[cut] + +## Method examples + +For the start, let's teach the `user` to say hello: + +```js run +let user = { + name: "John", + age: 30 +}; + +*!* +user.sayHi = function() { + alert("Hello!"); +}; +*/!* + +user.sayHi(); // Hello! +``` + +Here we've just used a Function Expression to create the function and assign it to the property `user.sayHi` of the object. + +Then we can call it. The user now can speak! + +A function that is the property of an object is called its *method*. + +So, here we've got a method `sayHi` of the object `user`. + +Of course, we could use a pre-declared function as a method, like this: + +```js run +let user = { + // ... +}; + +*!* +// first, declare +function sayHi() { + alert("Hello!"); +}; + +// then add as a method +user.sayHi = sayHi; +*/!* + +user.sayHi(); // Hello! +``` + +```smart header="Object-oriented programming" +When we write our code using objects to represent entities, that's called an [object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming), in short: "OOP". + +OOP is a big thing, an interesting science of its own. How to choose the right entities? How to organize the interaction between them? That's architecture, and there are great books on that topic, like "Design Patterns: Elements of Reusable Object-Oriented Software" by E.Gamma, R.Helm, R.Johnson, J.Vissides or "Object-Oriented Analysis and Design with Applications" by G.Booch, and more. We'll scratch the surface of that topic later in the chapter . +``` +### Method shorthand + +There exists a shorter syntax for methods in an object literal: + +```js +// these objects do the same + +let user = { + sayHi: function() { + alert("Hello"); + } +}; + +// method shorthand looks better, right? +let user = { +*!* + sayHi() { // same as "sayHi: function()" +*/!* + alert("Hello"); + } +}; +``` + +As demonstrated, we can omit `"function"` and just write `sayHi()`. + +To say the truth, the notations are not fully identical. There are subtle differences related to object inheritance (to be covered later), but for now they do not matter. In almost all cases the shorter syntax is preferred. + +## "this" in methods + +It's common that an object method needs to access the information stored in the object to do its job. + +For instance, the code inside `user.sayHi()` may need the name of the `user`. + +**To access the object, a method can use the `this` keyword.** + +The value of `this` is the object "before dot", the one used to call the method. + +For instance: + +```js run +let user = { + name: "John", + age: 30, + + sayHi() { +*!* + alert(this.name); +*/!* + } + +}; + +user.sayHi(); // John +``` + +Here during the execution of `user.sayHi()`, the value of `this` will be `user`. + +Technically, it's also possible to access the object without `this`, by referencing it via the outer variable: + +```js +let user = { + name: "John", + age: 30, + + sayHi() { +*!* + alert(user.name); // "user" instead of "this" +*/!* + } + +}; +``` + +...But such code is unreliable. If we decide to copy `user` to another variable, e.g. `admin = user` and overwrite `user` with something else, then it will access the wrong object. + +That's demonstrated below: + +```js run +let user = { + name: "John", + age: 30, + + sayHi() { +*!* + alert( user.name ); // leads to an error +*/!* + } + +}; + + +let admin = user; +user = null; // overwrite to make things obvious + +admin.sayHi(); // Whoops! inside sayHi(), the old name is used! error! +``` + +If we used `this.name` instead of `user.name` inside the `alert`, then the code would work. + +## "this" is not bound + +In JavaScript, "this" keyword behaves unlike most other programming languages. First, it can be used in any function. + +There's no syntax error in the code like that: + +```js +function sayHi() { + alert( *!*this*/!*.name ); +} +``` + +The value of `this` is evaluated during the run-time. And it can be anything. + +For instance, the same function may have different "this" when called from different objects: + +```js run +let user = { name: "John" }; +let admin = { name: "Admin" }; + +function sayHi() { + alert( this.name ); +} + +*!* +// use the same functions in two objects +user.f = sayHi; +admin.f = sayHi; +*/!* + +// these calls have different this +// "this" inside the function is the object "before the dot" +user.f(); // John (this == user) +admin.f(); // Admin (this == admin) + +admin['f'](); // Admin (dot or square brackets access the method – doesn't matter) +``` + +Actually, we can call the function without an object at all: + +```js run +function sayHi() { + alert(this); +} + +sayHi(); // undefined +``` + +In this case `this` is `undefined` in strict mode. If we try to access `this.name`, there will be an error. + +In non-strict mode (if one forgets `use strict`) the value of `this` in such case will be the *global object* (`window` in a browser, we'll get to it later). This is a historical behavior that `"use strict"` fixes. + +Please note that usually a call of a function that uses `this` without an object is not normal, but rather a programming mistake. If a function has `this`, then it is usually meant to be called in the context of an object. + +```smart header="The consequences of unbound `this`" +If you come from another programming language, then you are probably used to the idea of a "bound `this`", where methods defined in an object always have `this` referencing that object. + +In JavaScript `this` is "free", its value is evaluated at call-time and does not depend on where the method was declared, but rather on what's the object "before the dot". + +The concept of run-time evaluated `this` has both pluses and minuses. On the one hand, a function can be reused for different objects. On the the other hand, greater flexibility opens a place for mistakes. + +Here our position is not to judge whether this language design decision is good or bad. We'll understand how to work with it, how to get benefits and evade problems. +``` + +## Internals: Reference Type + +```warn header="In-depth language feature" +This section covers an advanced topic, to understand certain edge-cases better. + +If you want to go on faster, it can be skipped or postponed. +``` + +An intricate method call can lose `this`, for instance: + +```js run +let user = { + name: "John", + hi() { alert(this.name); }, + bye() { alert("Bye"); } +}; + +user.hi(); // John (the simple call works) + +*!* +// now let's call user.hi or user.bye depending on the name +(user.name == "John" ? user.hi : user.bye)(); // Error! +*/!* +``` + +On the last line there is a ternary operator that chooses either `user.hi` or `user.bye`. In this case the result is `user.hi`. + +The method is immediately called with parentheses `()`. But it doesn't work right! + +You can see that the call results in an error, cause the value of `"this"` inside the call becomes `undefined`. + +This works (object dot method): +```js +user.hi(); +``` + +This doesn't (evaluated method): +```js +(user.name == "John" ? user.hi : user.bye)(); // Error! +``` + +Why? If we want to understand why it happens -- let's get under the hood of how `obj.method()` call works. + +Looking closely, we may notice two operations in `obj.method()` statement: + +1. First, the dot `'.'` retrieves the property `obj.method`. +2. Then parentheses `()` execute it. + +So, how the information about `this` gets passed from the first part to the second one? + +If we put these operations on separate lines, then `this` will be lost for sure: + +```js run +let user = { + name: "John", + hi() { alert(this.name); } +} + +*!* +// split getting and calling the method in two lines +let hi = user.hi; +hi(); // Error, because this is undefined +*/!* +``` + +Here `hi = user.hi` puts the function into the variable, and then on the last line it is completely standalone, and so there's no `this`. + +**To make `user.hi()` calls work, JavaScript uses a trick -- the dot `'.'` returns not a function, but a value of the special [Reference Type](https://tc39.github.io/ecma262/#sec-reference-specification-type).** + +The Reference Type is a "specification type". We can't explicitly use it, but it is used internally by the language. + +The value of Reference Type is a three-value combination `(base, name, strict)`, where: + +- `base` is the object. +- `name` is the property. +- `strict` is true if `use strict` is in effect. + +The result of a property access `user.hi` is not a function, but a value of Reference Type. For `user.hi` in strict mode it is: + +```js +// Reference Type value +(user, "hi", true) +``` + +When parentheses `()` are called on the Reference Type, they receive the full information about the object and it's method, and can set the right `this` (`=user` in this case). + +Any other operation like assignment `hi = user.hi` discards the reference type as a whole, takes the value of `user.hi` (a function) and passes it on. So any further operation "looses" `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). + +## 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. + +For instance, here `arrow()` uses `this` from the outer `user.sayHi()` method: + +```js run +let user = { + firstName: "Ilya", + sayHi() { + let arrow = () => alert(this.firstName); + arrow(); + } +}; + +user.sayHi(); // Ilya +``` + +That's a special feature of arrow functions, it's useful when we actually do not want to have a separate `this`, but rather to take it from the outer context. Later in the chapter we'll go more deeply into arrow functions. + + +## Summary + +- Functions that are stored in object properties are called "methods". +- Methods allow objects to "act" like `object.doSomething()`. +- Methods can reference the object as `this`. + +The value of `this` is defined at run-time. +- When a function is declared, it may use `this`, but that `this` has no value until the function is called. +- That function can be copied between objects. +- When a function is called in the "method" syntax: `object.method()`, the value of `this` during the call is `object`. + +Please note that arrow functions are special: they have no `this`. When `this` is accessed inside an arrow function -- it is taken from outside. diff --git a/1-js/04-object-basics/05-object-toprimitive/article.md b/1-js/04-object-basics/05-object-toprimitive/article.md new file mode 100644 index 00000000..d39fe523 --- /dev/null +++ b/1-js/04-object-basics/05-object-toprimitive/article.md @@ -0,0 +1,238 @@ + +# Object to primitive conversion + +What happens when objects are added `obj1 + obj2`, substracted `obj1 - obj2` or printed using `alert(obj)`? + +There are special methods in objects that do the conversion. + +In the chapter we've seen the rules for numeric, string and boolean conversions of primitives. But we left a gap for objects. Now, as we know about methods and symbols it becomes possible to close it. + +[cut] + +For objects, there's no to-boolean conversion, because all objects are `true` in a boolean context. So there are only string and numeric conversions. + +The numeric conversion happens when we substract objects or apply mathematical functions. For instance, `Date` objects (to be covered in the chapter ) can be substracted, and the result of `date1 - date2` is the time difference between two dates. + +As for the string conversion -- it usually happens when we output an object like `alert(obj)` and in similar contexts. + +## ToPrimitive + +When an object is used in the context where a primitive is required, for instance, in an `alert` or mathematical operations, it's converted to a primitive value using the `ToPrimitive` algorithm ([specification](https://tc39.github.io/ecma262/#sec-toprimitive)). + +That algorithm allows to customize the conversion using in a special object method. + +Depending on the context, the conversion has a so-called "hint". + +There are 3 variants: + +`"string"` +: When an operation expects a string, for object-to-string conversions, like `alert`: + + ```js + // output + alert(obj); + + // using object as a property key + anotherObj[obj] = 123; + ``` + +`"number"` +: When an operation expects a number, for object-to-number conversions, like maths: + + ```js + // explicit conversion + let num = Number(obj); + + // maths (except binary plus) + let n = +obj; // unary plus + let delta = date1 - date2; + + // less/greater comparison + let greater = user1 > user2; + ``` + +`"default"` +: Occurs in rare cases when the operator is "not sure" what type to expect. + + For instance, binary plus `+` can work both with strings (concatenates them) and numbers (adds them), so both strings and numbers would do. Or when an object is compared using `==` with a string, number or a symbol. + + ```js + // binary plus + let total = car1 + car2; + + // obj == string/number/symbol + if (user == 1) { ... }; + ``` + + The greater/less operator `<>` can work with both strings and numbers too. Still, it uses "number" hint, not "default". That's for historical reasons. + + In practice, all built-in objects except for one case (`Date` object, we'll learn it later) implement `"default"` conversion the same way as `"number"`. And probably we should do the same. + +Please note -- there are only three hints. That simple. There is no "boolean" hint (all objects are `true` in boolean context) or anything else. And if we treat `"default"` and `"number"` the same, like most built-ins do, then there are only two conversions. + +**To do the conversion, JavaScript tries to find and call three object methods:** + +1. Call `obj[Symbol.toPrimitive](hint)` if the method exists, +2. Otherwise if hint is `"string"` + - try `obj.toString()` and `obj.valueOf()`, whatever exists. +3. Otherwise if hint is `"number"` or `"default"` + - try `obj.valueOf()` and `obj.toString()`, whatever exists. + +## Symbol.toPrimitive + +Let's start from the first method. There's a built-in symbol named `Symbol.toPrimitive` that should be used to name the conversion method, like this: + +```js +obj[Symbol.toPrimitive] = function(hint) { + // return a primitive value + // hint = one of "string", "number", "default" +} +``` + +For instance, here `user` object implements it: + +```js run +let user = { + name: "John", + money: 1000, + + [Symbol.toPrimitive](hint) { + alert(`hint: ${hint}`); + return hint == "string" ? `{name: "${this.name}"}` : this.money; + } +}; + +// conversions demo: +alert(user); // hint: string -> {name: "John"} +alert(+user); // hint: number -> 1000 +alert(user + 500); // hint: default -> 1500 +``` + +As we can see from the code, `user` becomes a self-descriptive string or a money amount depending on the conversion. The single method `user[Symbol.toPrimitive]` handles all conversion cases. + + +## toString/valueOf + +Methods `toString` and `valueOf` come from ancient times. They are not symbols (symbols did not exist that long ago), but rather "regular" string-named methods. They provide an alternative "old-style" way to implement the conversion. + +If there's no `Symbol.toPrimitive` then JavaScript tries to find them and try in the order: + +- `toString -> valueOf` for "string" hint. +- `valueOf -> toString` otherwise. + +For instance, here `user` does the same as above using a combination of `toString` and `valueOf`: + +```js run +let user = { + name: "John", + money: 1000, + + // for hint="string" + toString() { + return `{name: "${this.name}"}`; + }, + + // for hint="number" or "default" + valueOf() { + return this.money; + } + +}; + +alert(user); // toString -> {name: "John"} +alert(+user); // valueOf -> 1000 +alert(user + 500); // valueOf -> 1500 +``` + +Often we want a single "catch-all" place to handle all primitive conversions. In this case we can implement `toString` only, like this: + +```js run +let user = { + name: "John", + + toString() { + return this.name; + } +}; + +alert(user); // toString -> John +alert(user + 500); // toString -> John500 +``` + +In the absense of `Symbol.toPrimitive` and `valueOf`, `toString` will handle all primitive conversions. + + +## ToPrimitive and ToString/ToNumber + +The important thing to know about all primitive-conversion methods is that they not necessarily return the "hinted" primitive. + +There is no control whether `toString()` returns exactly a string, or whether `Symbol.toPrimitive` method returns a number for a hint "number". + +**The only mandatory thing: these methods must return a primitive.** + +An operation that initiated the conversion gets that primitive, and then continues to work with it, applying further conversions if necessary. + +For instance: + +- Mathematical operations (except binary plus) perform `ToNumber` conversion: + + ```js run + let obj = { + toString() { // toString handles all conversions in the absense of other methods + return "2"; + } + }; + + alert(obj * 2); // 4, ToPrimitive gives "2", then it becomes 2 + ``` + +- Binary plus performs checks the primitive -- if it's a string, then it does concatenation, otherwise performs `ToNumber` and works with numbers. + + String example: + ```js run + let obj = { + toString() { + return "2"; + } + }; + + alert(obj + 2); // 22 (ToPrimitive returned string => concatenation) + ``` + + Number example: + ```js run + let obj = { + toString() { + return true; + } + }; + + alert(obj + 2); // 3 (ToPrimitive returned boolean, not string => ToNumber) + ``` + +```smart header="Historical notes" +For historical reasons, methods `toString` or `valueOf` *should* return a primitive: if any of them returns an object, then there's no error, but the object is ignored (like if the method didn't exist). + +In contrast, `Symbol.toPrimitive` *must* return an primitive, otherwise there will be an error. +``` + +## Summary + +The object-to-primitive conversion is called automatically by many built-in functions and operators that expect a primitive as a value. + +There are 3 types (hints) of it: +- `"string"` (for `alert` and other string conversions) +- `"number"` (for maths) +- `"default"` (few operators) + +The specification describes explicitly which operator uses which hint. There are very few operators that "don't know what to expect" and use the `"default"` hint. Usually for built-in objects `"default"` hint is handled the same way as `"number"`, so in practice the last two are often merged together. + +The conversion algorithm is: + +1. Call `obj[Symbol.toPrimitive](hint)` if the method exists, +2. Otherwise if hint is `"string"` + - try `obj.toString()` and `obj.valueOf()`, whatever exists. +3. Otherwise if hint is `"number"` or `"default"` + - try `obj.valueOf()` and `obj.toString()`, whatever exists. + +In practice, it's often enough to implement only `obj.toString()` as a "catch-all" method for all conversions that returns a "human-readable" representation of an object, for logging or debugging purposes. diff --git a/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/solution.md b/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/solution.md new file mode 100644 index 00000000..79a29b0d --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/solution.md @@ -0,0 +1,14 @@ +Yes, it's possible. + +If a function returns an object then `new` returns it instead of `this`. + +So thay can, for instance, return the same externally defined object `obj`: + +```js run no-beautify +let obj = {}; + +function A() { return obj; } +function B() { return obj; } + +alert( new A() == new B() ); // true +``` diff --git a/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/task.md b/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/task.md new file mode 100644 index 00000000..8c1fea8e --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/task.md @@ -0,0 +1,19 @@ +importance: 2 + +--- + +# Two functions – one object + +Is it possible to create functions `A` and `B` such as `new A()==new B()`? + +```js no-beautify +function A() { ... } +function B() { ... } + +let a = new A; +let b = new B; + +alert( a == b ); // true +``` + +If it is, then provide an example of their code. diff --git a/1-js/6-objects-more/3-constructor-new/2-calculator-constructor/_js.view/solution.js b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/_js.view/solution.js similarity index 100% rename from 1-js/6-objects-more/3-constructor-new/2-calculator-constructor/_js.view/solution.js rename to 1-js/04-object-basics/06-constructor-new/2-calculator-constructor/_js.view/solution.js diff --git a/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/_js.view/test.js b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/_js.view/test.js new file mode 100644 index 00000000..03605392 --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/_js.view/test.js @@ -0,0 +1,25 @@ + +describe("calculator", function() { + let calculator; + before(function() { + sinon.stub(window, "prompt") + + prompt.onCall(0).returns("2"); + prompt.onCall(1).returns("3"); + + calculator = new Calculator(); + calculator.read(); + }); + + it("when 2 and 3 are entered, the sum is 5", function() { + assert.equal(calculator.sum(), 5); + }); + + it("when 2 and 3 are entered, the product is 6", function() { + assert.equal(calculator.mul(), 6); + }); + + after(function() { + prompt.restore(); + }); +}); diff --git a/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/solution.md b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/solution.md new file mode 100644 index 00000000..e5583c5d --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/solution.md @@ -0,0 +1,25 @@ + + +```js run demo +function Calculator() { + + this.read = function() { + this.a = +prompt('a?', 0); + this.b = +prompt('b?', 0); + }; + + this.sum = function() { + return this.a + this.b; + }; + + this.mul = function() { + return this.a * this.b; + }; +} + +let calculator = new Calculator(); +calculator.read(); + +alert( "Sum=" + calculator.sum() ); +alert( "Mul=" + calculator.mul() ); +``` diff --git a/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/task.md b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/task.md new file mode 100644 index 00000000..60e7c373 --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/task.md @@ -0,0 +1,23 @@ +importance: 5 + +--- + +# Create new Calculator + +Create a constructor function `Calculator` that creates objects with 3 methods: + +- `read()` asks for two values using `prompt` and remembers them in object properties. +- `sum()` returns the sum of these properties. +- `mul()` returns the multiplication product of these properties. + +For instance: + +```js +let calculator = new Calculator(); +calculator.read(); + +alert( "Sum=" + calculator.sum() ); +alert( "Mul=" + calculator.mul() ); +``` + +[demo] diff --git a/1-js/04-object-basics/06-constructor-new/3-accumulator/_js.view/solution.js b/1-js/04-object-basics/06-constructor-new/3-accumulator/_js.view/solution.js new file mode 100644 index 00000000..585287c5 --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/3-accumulator/_js.view/solution.js @@ -0,0 +1,8 @@ +function Accumulator(startingValue) { + this.value = startingValue; + + this.read = function() { + this.value += +prompt('How much to add?', 0); + }; + +} diff --git a/1-js/04-object-basics/06-constructor-new/3-accumulator/_js.view/test.js b/1-js/04-object-basics/06-constructor-new/3-accumulator/_js.view/test.js new file mode 100644 index 00000000..a719cf45 --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/3-accumulator/_js.view/test.js @@ -0,0 +1,30 @@ +describe("Accumulator", function() { + + beforeEach(function() { + sinon.stub(window, "prompt") + }); + + afterEach(function() { + prompt.restore(); + }); + + it("initial value is the argument of the constructor", function() { + let accumulator = new Accumulator(1); + + assert.equal(accumulator.value, 1); + }); + + it("after reading 0, the value is 1", function() { + let accumulator = new Accumulator(1); + prompt.returns("0"); + accumulator.read(); + assert.equal(accumulator.value, 1); + }); + + it("after reading 1, the value is 2", function() { + let accumulator = new Accumulator(1); + prompt.returns("1"); + accumulator.read(); + assert.equal(accumulator.value, 2); + }); +}); diff --git a/1-js/04-object-basics/06-constructor-new/3-accumulator/solution.md b/1-js/04-object-basics/06-constructor-new/3-accumulator/solution.md new file mode 100644 index 00000000..eb145e79 --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/3-accumulator/solution.md @@ -0,0 +1,17 @@ + + +```js run demo +function Accumulator(startingValue) { + this.value = startingValue; + + this.read = function() { + this.value += +prompt('How much to add?', 0); + }; + +} + +let accumulator = new Accumulator(1); +accumulator.read(); +accumulator.read(); +alert(accumulator.value); +``` diff --git a/1-js/04-object-basics/06-constructor-new/3-accumulator/task.md b/1-js/04-object-basics/06-constructor-new/3-accumulator/task.md new file mode 100644 index 00000000..3362b5b4 --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/3-accumulator/task.md @@ -0,0 +1,25 @@ +importance: 5 + +--- + +# Create new Accumulator + +Create a constructor function `Accumulator(startingValue)`. + +Object that it creates should: + +- Store the "current value" in the property `value`. The starting value is set to the argument of the constructor `startingValue`. +- The `read()` method should use `prompt` to read a new number and add it to `value`. + +In other words, the `value` property is the sum of all user-entered values with the initial value `startingValue`. + +Here's the demo of the code: + +```js +let accumulator = new Accumulator(1); // initial value 1 +accumulator.read(); // adds the user-entered value +accumulator.read(); // adds the user-entered value +alert(accumulator.value); // shows the sum of these values +``` + +[demo] diff --git a/1-js/04-object-basics/06-constructor-new/4-calculator-extendable/_js.view/solution.js b/1-js/04-object-basics/06-constructor-new/4-calculator-extendable/_js.view/solution.js new file mode 100644 index 00000000..50c40e80 --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/4-calculator-extendable/_js.view/solution.js @@ -0,0 +1,25 @@ +function Calculator() { + + let methods = { + "-": (a, b) => a - b, + "+": (a, b) => a + b + }; + + this.calculate = function(str) { + + let split = str.split(' '), + a = +split[0], + op = split[1], + b = +split[2] + + if (!methods[op] || isNaN(a) || isNaN(b)) { + return NaN; + } + + return methods[op](a, b); + } + + this.addMethod = function(name, func) { + methods[name] = func; + }; +} diff --git a/1-js/04-object-basics/06-constructor-new/4-calculator-extendable/_js.view/test.js b/1-js/04-object-basics/06-constructor-new/4-calculator-extendable/_js.view/test.js new file mode 100644 index 00000000..eac4f54a --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/4-calculator-extendable/_js.view/test.js @@ -0,0 +1,25 @@ +describe("Calculator", function() { + let calculator; + + before(function() { + calculator = new Calculator; + }); + + it("calculate(12 + 34) = 46", function() { + assert.equal(calculator.calculate("12 + 34"), 46); + }); + + it("calculate(34 - 12) = 22", function() { + assert.equal(calculator.calculate("34 - 12"), 22); + }); + + it("add multiplication: calculate(2 * 3) = 6", function() { + calculator.addMethod("*", (a, b) => a * b); + assert.equal(calculator.calculate("2 * 3"), 6); + }); + + it("add power: calculate(2 ** 3) = 8", function() { + calculator.addMethod("**", (a, b) => a ** b); + assert.equal(calculator.calculate("2 ** 3"), 8); + }); +}); diff --git a/1-js/04-object-basics/06-constructor-new/4-calculator-extendable/solution.md b/1-js/04-object-basics/06-constructor-new/4-calculator-extendable/solution.md new file mode 100644 index 00000000..41178663 --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/4-calculator-extendable/solution.md @@ -0,0 +1,3 @@ + +- Please note how methods are stored. They are simply added to the internal object. +- All tests and numeric conversions are done in the `calculate` method. In future it may be extended to support more complex expressions. diff --git a/1-js/04-object-basics/06-constructor-new/4-calculator-extendable/task.md b/1-js/04-object-basics/06-constructor-new/4-calculator-extendable/task.md new file mode 100644 index 00000000..1f6368c0 --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/4-calculator-extendable/task.md @@ -0,0 +1,36 @@ +importance: 5 + +--- + +# Create an extendable calculator + +Create a constructor function `Calculator` that creates "extendable" calculator objects. + +The task consists of two parts. + +1. First, implement the method `calculate(str)` that takes a string like `"1 + 2"` in the format "NUMBER operator NUMBER" (space-delimited) and returns the result. Should understand plus `+` and minus `-`. + + Usage example: + + ```js + let calc = new Calculator; + + alert( calc.calculate("3 + 7") ); // 10 + ``` +2. Then add the method `addOperator(name, func)` that teaches the calculator a new operation. It takes the operator `name` and the two-argument function `func(a,b)` that implements it. + + For instance, let's add the multiplication `*`, division `/` and power `**`: + + ```js + let powerCalc = new Calculator; + powerCalc.addMethod("*", (a, b) => a * b); + powerCalc.addMethod("/", (a, b) => a / b); + powerCalc.addMethod("**", (a, b) => a ** b); + + let result = powerCalc.calculate("2 ** 3"); + alert( result ); // 8 + ``` + +- No brackets or complex expressions in this task. +- The numbers and the operator are delimited with exactly one space. +- There may be error handling if you'd like to add it. diff --git a/1-js/04-object-basics/06-constructor-new/article.md b/1-js/04-object-basics/06-constructor-new/article.md new file mode 100644 index 00000000..f2c43ab6 --- /dev/null +++ b/1-js/04-object-basics/06-constructor-new/article.md @@ -0,0 +1,219 @@ +# Constructor, operator "new" + +The regular `{...}` syntax allows to create one object. But often we need to create many similar objects. + +That can be done using constructor functions and the `"new"` operator. + +[cut] + +## Constructor function + +Constructor functions technically are regular functions. There are two conventions though: + +1. They are named with capital letter first. +2. They should be executed only with `"new"` operator. + +For instance: + +```js run +function User(name) { + this.name = name; + this.isAdmin = false; +} + +*!* +let user = new User("Jack"); +*/!* + +alert(user.name); // Jack +alert(user.isAdmin); // false +``` + +When a function is executed as `new User(...)`, it does the following steps: + +1. A new empty object is created and assigned to `this`. +2. The function executes. Usually it modifies `this`, adds new properties to it. +3. The value of `this` is returned. + +In other words, `new User(...)` does something like: + +```js +function User(name) { +*!* + // this = {}; (implicitly) +*/!* + + // we add properties to this + this.name = name; + this.isAdmin = false; + +*!* + // return this; (implicitly) +*/!* +} +``` + +So the result of `new User("Jack")` is the same object as: + +```js +let user = { + name: "Jack", + isAdmin: false +}; +``` + +Now if we want to create other users, we can call `new User("Ann")`, `new User("Alice")` and so on. Much shorter than using literals every time, and also reads well. + +That's the main purpose of constructors -- to implement reusable object creation code. + +Let's note once again -- technically, any function can be used as a constructor. That is: any function can be run with `new`, and it will execute the algorithm above. The "capital letter first" is a common agreement, to make it clear that a function is to be run with `new`. + +````smart header="new function() { ... }" +If we have many lines of code all about creation of a single complex object, we can wrap them in constructor function, like this: + +```js +let user = new function() { + this.name = "John"; + this.isAdmin = false; + + // ...other code for user creation + // maybe complex logic and statements + // local variables etc +}; +``` + +The constructor can't be called again, because it is not saved anywhere, just created and called. So this trick aims to encapsulate the code for a single complex object only. +```` + +## Dual-use constructors: new.target + +Inside a function, we can check whether it was called with `new` or without it, using a special `new.target` property. + +It is empty for ordinary runs and equals the function if called with `new`: + +```js run +function User() { + alert(new.target); +} + +User(); // undefined +new User(); // function User { ... } +``` + +That can be used to allow both `new` and ordinary syntax work the same: + + +```js run +function User(name) { + if (!new.target) { // if you run me without new + return new User(name); // ...I will add new for you + } + + this.name = name; +} + +let john = User("John"); // redirects call to new User +alert(john.name); // John +``` + +This approach is sometimes used in libraries to make the syntax more flexible. Probably not a good thing to use everywhere though, because it makes a bit less obvious what's going on for a person who's familiar with the internals of `User`. + +## Return from constructors + +Usually, constructors do not have a `return` statement. Their task is to write all necessary stuff into `this`, and it automatically becomes the result. + +But if there is a `return` statement, then the rule is simple: + +- If `return` is called with object, then it is returned instead of `this`. +- If `return` is called with a primitive, it's ignored. + +In other words, `return` with an object returns that object, otherwise `this` is returned. + +For instance, here `return` overrides `this` by returning an object: + +```js run +function BigUser() { + + this.name = "John"; + + return { name: "Godzilla" }; // <-- returns an object +} + +alert( new BigUser().name ); // Godzilla, got that object +``` + +And here's an example with an empty `return` (or we could place a primitive after it, doesn't matter): + +```js run +function SmallUser() { + + this.name = "John"; + + return; // finishes the execution, returns this + + // ... + +} + +alert( new SmallUser().name ); // John +``` + +Most of the time constructors return nothing. Here we mention the special behavior with returning objects mainly for the sake of completeness. + +````smart header="Omitting brackets" +By the way, we can omit brackets after `new`, if it has no arguments: + +```js +let user = new User; // <-- no brackets +// same as +let user = new User(); +``` + +Omitting brackets here is not considered a "good style", but the syntax is permitted by specification. +```` + +## Methods in constructor + +Using constuctor functions to create objects gives a great deal of flexibility. The constructor function may have parameters that define how to construct the object, what to put in it. + +Of course, we can add to `this` not only properties, but methods as well. + +For instance, `new User(name)` below creates an object with the given `name` and the method `sayHi`: + +```js run +function User(name) { + this.name = name; + + this.sayHi = function() { + alert( "My name is: " + this.name ); + }; +} + +*!* +let john = new User("John"); + +john.sayHi(); // My name is: John +*/!* + +/* +john = { + name: "John", + sayHi: function() { ... } +} +*/ +``` + +## Summary + +- Constructor functions or, shortly, constructors, are regular functions, but there's a common agreement to name them with capital letter first. +- Constructor functions should only be called using `new`. Such call implies a creation of empty `this` at the start and returning the populated one at the end. + +We can use constructor functions to make multiple similar objects. + +JavaScript provides constructor functions for many built-in language objects: like `Date` for dates, `Set` for sets and others that we plan to study. + +```smart header="Objects, we'll be back!" +In this chapter we only cover the basics about objects. They are essential for learning more about data types and functions in the next chapters. + +After we learn that, in the chapter we return to objects and cover them in-depth, including inheritance and classes. +``` diff --git a/1-js/04-object-basics/index.md b/1-js/04-object-basics/index.md new file mode 100644 index 00000000..d2387aaf --- /dev/null +++ b/1-js/04-object-basics/index.md @@ -0,0 +1 @@ +# Objects: the basics diff --git a/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md b/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md new file mode 100644 index 00000000..a169f776 --- /dev/null +++ b/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md @@ -0,0 +1,31 @@ + +Try running it: + +```js run +let str = "Hello"; + +str.test = 5; // (*) + +alert(str.test); +``` + +There may be two kinds of result: +1. `undefined` +2. An error. + +Why? Let's replay what's happening at line `(*)`: + +1. When a property of `str` is accessed, a "wrapper object" is created. +2. The operation with the property is carried out on it. So, the object gets the `test` property. +3. The operation finishes and the "wrapper object" disappears. + +So, on the last line, `str` has no trace of the property. A new wrapper object for every object operation on a string. + +Some browsers though may decide to further limit the programmer and disallow to assign properties to primitives at all. That's why in practice we can also see errors at line `(*)`. It's a little bit farther from the specification though. + +**This example clearly shows that primitives are not objects.** + +They just can not store data. + +All property/method operations are performed with the help of temporary objects. + diff --git a/1-js/05-data-types/01-primitives-methods/1-string-new-property/task.md b/1-js/05-data-types/01-primitives-methods/1-string-new-property/task.md new file mode 100644 index 00000000..50c781ea --- /dev/null +++ b/1-js/05-data-types/01-primitives-methods/1-string-new-property/task.md @@ -0,0 +1,18 @@ +importance: 5 + +--- + +# Can I add a string property? + + +Consider the following code: + +```js +let str = "Hello"; + +str.test = 5; + +alert(str.test); +``` + +How do you think, will it work? What will be shown? diff --git a/1-js/05-data-types/01-primitives-methods/article.md b/1-js/05-data-types/01-primitives-methods/article.md new file mode 100644 index 00000000..73dc10ca --- /dev/null +++ b/1-js/05-data-types/01-primitives-methods/article.md @@ -0,0 +1,129 @@ +# Methods of primitives + +JavaScript allows to work with primitives (strings, numbers etc) as if they were objects. + +They also provide methods to call and such. We are going to study them soon, but first let's see how it works, because, of course, primitives are not objects (and here we plan to make it even more clear). + +[cut] + +Let's formulate the key distinction 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`. + +An object +: Is capable of storing multiple values as properties. +Can be created with `{}`, for instance: `{name: "John", age: 30}`. There are other kinds of objects in JavaScript, e.g. functions are objects. + +One of the best things about objects is that we can store a function as one of properties: + +```js run +let john = { + name: "John", + sayHi: function() { + alert("Hi buddy!"); + } +}; + +john.sayHi(); // Hi buddy! +``` + +So, here we've made an object `john` with the method `sayHi`. + +There exist many built-in objects, including those that work with dates, errors, HTML elements etc. They have different properties and methods. + +But features come at a price! + +Objects are "heavier" than primitives. They require additional resources to support the internal machinery. But properties and methods are useful in programming, JavaScript engines try to optimize them, so the price is usually fair. + +## A primitive as an object + +Here's the paradox faced by the creator of JavaScript: + +- There are many things one would want to do with a primitive like a string or a number. Could be great to access them as methods. +- Primitives must be as fast and lightweight as possible. + +The solution looks a little bit awkward, but here it is. + +1. Primitives are still primitive. A single value, as desired. +2. The language allows to access methods and properties of strings, numbers, booleans and symbols. +3. When it happens, a special "object wrapper" is created that provides the functionality and then is destroyed. + +The "object wrappers" are different for each primitive type and are named specifically: `String`, `Number`, `Boolean` and `Symbol`. Thus they provide different sets of methods. + +For instance, there exists a method [str.toUpperCase()](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) that returns the capitalized string. + +Here's how it works: + +```js run +let str = "Hello"; + +alert( str.toUpperCase() ); // HELLO +``` + +Simple, right? And here's what actually happens in `str.toUpperCase()`: + +1. The string `str` is a primitive. So in the moment of accessing its property a special object is created that both knows the value of the string and has useful methods, like `toUpperCase()`. +2. That method runs and returns a new string (shown by `alert`). +3. The special object is destroyed, leaving the primitive `str` alone. + +So, primitives can provide methods, but they still remain lightweight. + +Of course, a JavaScript engine highly optimizes that process. Internally it may skip the creation of the extra object at all. But it must adhere to the specification and behave as if it creates one. + +A number has methods of it's own, for instance, [toFixed(n)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed) rounds the number to the given precision: + +```js run +let n = 1.23456; + +alert( n.toFixed(2) ); // 1.23 +``` + +We'll see more specific methods in chapters and . + + +````warn header="Constructors `String/Number/Boolean` are for internal use only" +Some languages like Java allow to create "wrapper objects" for primitives explicitly using syntax like `new Number(1)` or `new Boolean(false)`. + +In JavaScript that's also possible for historical reasons, but highly **not recommended**. Things will go crazy in many places. + +For instance: + +```js run +alert( typeof 1 ); // "number" + +alert( typeof new Number(1) ); // "object"! +``` + +And, because in what follows `zero` is an object, the alert will show up: + +```js run +let zero = new Number(0); + +if (zero) { // zero is true, because it's an object + alert( "zero is truthy?!?" ); +} +``` + +From the other side, using the same functions `String/Number/Boolean` without `new` is a totally sane and useful thing. They convert a value to the corresponding type: to a string, a number, or a boolean (primitive). + +This is totally valid: +```js +let num = Number("123"); // convert a string to number +``` +```` + + +````warn header="null/undefined have no methods" +Special primitives `null` and `undefined` are exceptions. They have no corresponding "wrapper objects" and provide no methods. In a sense, they are "the most primitive". + +An attempt to access a property of such value would give an error: + +```js run +alert(null.test); // error +```` + +## Summary + +- Primitives except `null` and `undefined` provide many helpful methods. We plan to study those in the next chapters. +- Formally, these methods work via temporary objects, but JavaScript engines are very well tuned to optimize that internally, so they are not expensive to call. diff --git a/1-js/05-data-types/02-number/1-sum-interface/solution.md b/1-js/05-data-types/02-number/1-sum-interface/solution.md new file mode 100644 index 00000000..f2c81437 --- /dev/null +++ b/1-js/05-data-types/02-number/1-sum-interface/solution.md @@ -0,0 +1,12 @@ + + +```js run demo +let a = +prompt("The first number?", ""); +let b = +prompt("The second number?", ""); + +alert( a + b ); +``` + +Note the unary plus `+` before `prompt`. It immediately converts the value to a number. + +Otherwise, `a` and `b` would be string their sum would be their concatenation, that is: `"1" + "2" = "12"`. \ No newline at end of file diff --git a/1-js/05-data-types/02-number/1-sum-interface/task.md b/1-js/05-data-types/02-number/1-sum-interface/task.md new file mode 100644 index 00000000..78012664 --- /dev/null +++ b/1-js/05-data-types/02-number/1-sum-interface/task.md @@ -0,0 +1,11 @@ +importance: 5 + +--- + +# Sum numbers from the visitor + +Create a script that prompts the visitor to enter two numbers and then shows their sum. + +[demo] + +P.S. There is a gotcha with types. diff --git a/1-js/05-data-types/02-number/2-why-rounded-down/solution.md b/1-js/05-data-types/02-number/2-why-rounded-down/solution.md new file mode 100644 index 00000000..2152d73d --- /dev/null +++ b/1-js/05-data-types/02-number/2-why-rounded-down/solution.md @@ -0,0 +1,33 @@ +Internally the decimal fraction `6.35` is an endless binary. As always in such cases, it is stored with a precision loss. + +Let's see: + +```js run +alert( 6.35.toFixed(20) ); // 6.34999999999999964473 +``` + +The precision loss can cause both increase and decrease of a number. In this particular case the number becomes a tiny bit less, that's why it rounded down. + +And what's for `1.35`? + +```js run +alert( 1.35.toFixed(20) ); // 1.35000000000000008882 +``` + +Here the precision loss made the number a little bit greater, so it rounded up. + +**How can we fix the problem with `6.35` if we want it to be rounded the right way?** + +We should bring it closer to an integer prior to rounding: + +```js run +alert( (6.35 * 10).toFixed(20) ); // 63.50000000000000000000 +``` + +Note that `63.5` has no precision loss at all. That's because the decimal part `0.5` is actually `1/2`. Fractions divided by powers of `2` are exactly represented in the binary system, now we can round it: + + +```js run +alert( Math.round(6.35 * 10) / 10); // 6.35 -> 63.5 -> 63(rounded) -> 6.3 +``` + diff --git a/1-js/05-data-types/02-number/2-why-rounded-down/task.md b/1-js/05-data-types/02-number/2-why-rounded-down/task.md new file mode 100644 index 00000000..568c2648 --- /dev/null +++ b/1-js/05-data-types/02-number/2-why-rounded-down/task.md @@ -0,0 +1,22 @@ +importance: 4 + +--- + +# Why 6.35.toFixed(1) == 6.3? + +According to the documentation `Math.round` and `toFixed` both round to the nearest number: `0..4` lead down while `5..9` lead up. + +For instance: + +```js run +alert( 1.35.toFixed(1) ); // 1.4 +``` + +In the similar example below, why is `6.35` rounded to `6.3`, not `6.4`? + +```js run +alert( 6.35.toFixed(1) ); // 6.3 +``` + +How to round `6.35` the right way? + diff --git a/1-js/05-data-types/02-number/3-repeat-until-number/_js.view/solution.js b/1-js/05-data-types/02-number/3-repeat-until-number/_js.view/solution.js new file mode 100644 index 00000000..a8c30c01 --- /dev/null +++ b/1-js/05-data-types/02-number/3-repeat-until-number/_js.view/solution.js @@ -0,0 +1,12 @@ + +function readNumber() { + let num; + + do { + num = prompt("Enter a number please?", 0); + } while ( !isFinite(num) ); + + if (num === null || num === '') return null; + + return +num; +} \ No newline at end of file diff --git a/1-js/05-data-types/02-number/3-repeat-until-number/_js.view/test.js b/1-js/05-data-types/02-number/3-repeat-until-number/_js.view/test.js new file mode 100644 index 00000000..219fa806 --- /dev/null +++ b/1-js/05-data-types/02-number/3-repeat-until-number/_js.view/test.js @@ -0,0 +1,38 @@ +beforeEach(function() { + sinon.stub(window, "prompt"); +}); + +afterEach(function() { + prompt.restore(); +}); + +describe("readNumber", function() { + + it("if a number, returns it", function() { + prompt.returns("123"); + assert.strictEqual(readNumber(), 123); + }); + + it("if 0, returns it", function() { + prompt.returns("0"); + assert.strictEqual(readNumber(), 0); + }); + + it("continues the loop unti meets a number", function() { + prompt.onCall(0).returns("not a number"); + prompt.onCall(1).returns("not a number again"); + prompt.onCall(2).returns("1"); + assert.strictEqual(readNumber(), 1); + }); + + it("if an empty line, returns null", function() { + prompt.returns(""); + assert.isNull(readNumber()); + }); + + it("if cancel, returns null", function() { + prompt.returns(null); + assert.isNull(readNumber()); + }); + +}); \ No newline at end of file diff --git a/1-js/05-data-types/02-number/3-repeat-until-number/solution.md b/1-js/05-data-types/02-number/3-repeat-until-number/solution.md new file mode 100644 index 00000000..005116d1 --- /dev/null +++ b/1-js/05-data-types/02-number/3-repeat-until-number/solution.md @@ -0,0 +1,23 @@ + +```js run demo +function readNumber() { + let num; + + do { + num = prompt("Enter a number please?", 0); + } while ( !isFinite(num) ); + + if (num === null || num === '') return null; + + return +num; +} + +alert(`Read: ${readNumber()}`); +``` + +The solution is a little bit more intricate that it could be because we need to handle `null`/empty lines. + +So we actually accept the input until it is a "regular number". Both `null` (cancel) and empty line also fit that condition, because in numeric form they are `0`. + +After we stopped, we need to treat `null` and empty line specially (return `null`), because converting them to a number would return `0`. + diff --git a/1-js/05-data-types/02-number/3-repeat-until-number/task.md b/1-js/05-data-types/02-number/3-repeat-until-number/task.md new file mode 100644 index 00000000..9b172fa8 --- /dev/null +++ b/1-js/05-data-types/02-number/3-repeat-until-number/task.md @@ -0,0 +1,14 @@ +importance: 5 + +--- + +# Repeat until the input is a number + +Create a function `readNumber` which prompts for a number until the visitor enters a valid numeric value. + +The resulting value must be returned as a number. + +The visitor can also stop the process by entering an empty line or pressing "CANCEL". In that case, the function should return `null`. + +[demo] + diff --git a/1-js/05-data-types/02-number/4-endless-loop-error/solution.md b/1-js/05-data-types/02-number/4-endless-loop-error/solution.md new file mode 100644 index 00000000..8bc55bd0 --- /dev/null +++ b/1-js/05-data-types/02-number/4-endless-loop-error/solution.md @@ -0,0 +1,17 @@ +That's because `i` would never equal `10`. + +Run it to see the *real* values of `i`: + +```js run +let i = 0; +while (i < 11) { + i += 0.2; + if (i > 9.8 && i < 10.2) alert( i ); +} +``` + +None of them is exactly `10`. + +Such things happen because of the precision losses when adding fractions like `0.2`. + +Conclusion: evade equality checks when working with decimal fractions. \ No newline at end of file diff --git a/1-js/05-data-types/02-number/4-endless-loop-error/task.md b/1-js/05-data-types/02-number/4-endless-loop-error/task.md new file mode 100644 index 00000000..592ece31 --- /dev/null +++ b/1-js/05-data-types/02-number/4-endless-loop-error/task.md @@ -0,0 +1,15 @@ +importance: 4 + +--- + +# An occasional infinite loop + +This loop is infinite. It never ends. Why? + +```js +let i = 0; +while (i != 10) { + i += 0.2; +} +``` + diff --git a/1-js/05-data-types/02-number/8-random-min-max/solution.md b/1-js/05-data-types/02-number/8-random-min-max/solution.md new file mode 100644 index 00000000..348f9e34 --- /dev/null +++ b/1-js/05-data-types/02-number/8-random-min-max/solution.md @@ -0,0 +1,19 @@ +We need to "map" all values from the interval 0..1 into values from `min` to `max`. + +That can be done in two stages: + +1. If we multiply a random number from 0..1 by `max-min`, then it the interval of possible values increases `0..1` to `0..max-min`. +2. Now if we add `min`, the possible interval becomes from `min` to `max`. + +The function: + +```js run +function random(min, max) { + return min + Math.random() * (max - min); +} + +alert( random(1, 5) ); +alert( random(1, 5) ); +alert( random(1, 5) ); +``` + diff --git a/1-js/05-data-types/02-number/8-random-min-max/task.md b/1-js/05-data-types/02-number/8-random-min-max/task.md new file mode 100644 index 00000000..cde399db --- /dev/null +++ b/1-js/05-data-types/02-number/8-random-min-max/task.md @@ -0,0 +1,19 @@ +importance: 2 + +--- + +# A random number from min to max + +The built-in function `Math.random()` creates a random value from `0` to `1` (not including `1`). + +Write the function `random(min, max)` to generate a random floating-point number from `min` to `max` (not including `max`). + +Examples of its work: + +```js +alert( random(1, 5) ); // 1.2345623452 +alert( random(1, 5) ); // 3.7894332423 +alert( random(1, 5) ); // 4.3435234525 +``` + +You can use the solution of the [previous task](info:task/random-min-max) as the base. \ No newline at end of file diff --git a/1-js/05-data-types/02-number/9-random-int-min-max/solution.md b/1-js/05-data-types/02-number/9-random-int-min-max/solution.md new file mode 100644 index 00000000..793b7b30 --- /dev/null +++ b/1-js/05-data-types/02-number/9-random-int-min-max/solution.md @@ -0,0 +1,66 @@ +# The simple but wrong solution + +The simplest, but wrong solution would be to generate a value from `min` to `max` and round it: + +```js run +function randomInteger(min, max) { + let rnd = min + Math.random() * (max - min); + return Math.round(rnd); +} + +alert( randomInteger(1, 3) ); +``` + +The function works, but it is incorrect. The probability to get edge values `min` and `max` is two times less than any other. + +If you run the example above many times, you would easily see that `2` appears the most often. + +That happens because `Math.round()` gets random numbers from the interval `1..3` and rounds them as follows: + +```js no-beautify +values from 1 ... to 1.4999999999 become 1 +values from 1.5 ... to 2.4999999999 become 2 +values from 2.5 ... to 2.9999999999 become 3 +``` + +Now we can clearly see that `1` gets twice less values than `2`. And the same with `3`. + +# The correct solution + +There are many correct solutions to the task. One of them is to adjust interval borders. To ensure the same intervals, we can generate values from `0.5 to 2.5`, thus adding the required probabilities to the edges: + +```js run +*!* +function randomInteger(min, max) { + // now rnd is from (min-0.5) to (max+0.5) + let rnd = min - 0.5 + Math.random() * (max - min + 1); + return Math.round(rnd); +} +*/!* + +alert( randomInteger(1, 3) ); +``` + +An alternative way could be to use `Math.floor` for a random number from `min` to `max+1`: + +```js run +*!* +function randomInteger(min, max) { + // here rnd is from min to (max+1) + let rnd = min + Math.random() * (max + 1 - min); + return Math.floor(rand); +} +*/!* + +alert( randomInteger(1, 3) ); +``` + +Now all intervals are mapped this way: + +```js no-beautify +values from 1 ... to 1.9999999999 become 1 +values from 2 ... to 2.9999999999 become 2 +values from 3 ... to 3.9999999999 become 3 +``` + +All intervals have the same length, making the final distribution uniform. diff --git a/1-js/05-data-types/02-number/9-random-int-min-max/task.md b/1-js/05-data-types/02-number/9-random-int-min-max/task.md new file mode 100644 index 00000000..71d83f01 --- /dev/null +++ b/1-js/05-data-types/02-number/9-random-int-min-max/task.md @@ -0,0 +1,18 @@ +importance: 2 + +--- + +# A random integer from min to max + +Create a function `randomInteger(min, max)` that generates a random *integer* number from `min` to `max` including both `min` and `max` as possible values. + +Any number from the interval `min..max` must appear with the same probability. + + +Examples of its work: + +```js +alert( random(1, 5) ); // 1 +alert( random(1, 5) ); // 3 +alert( random(1, 5) ); // 5 +``` \ No newline at end of file diff --git a/1-js/05-data-types/02-number/article.md b/1-js/05-data-types/02-number/article.md new file mode 100644 index 00000000..ff07c661 --- /dev/null +++ b/1-js/05-data-types/02-number/article.md @@ -0,0 +1,437 @@ +# Numbers + +All numbers in JavaScript are stored in 64-bit format [IEEE-754](http://en.wikipedia.org/wiki/IEEE_754-1985) also known as "double precision". + +Let's recap what we know about them and add a little bit more. + +## More ways to write a number + +Imagine, we need to write a billion. The obvious way is: + +```js +let billion = 1000000000; +``` + +But in real life we usually dislike writing many zeroes. It's easy to mistype. Also we are lazy. We will usually write something like `"1bn"` for a billion or `"7.3bn"` for 7 billions 300 millions. The similar is true for other big numbers. + +In JavaScript, we can do almost the same by appending the letter `"e"` to the number and specifying the zeroes count: + +```js run +let billion = 1e9; // 1 billion, literally: 1 and 9 zeroes + +alert( 7.3e9 ); // 7.3 billions (7,300,000,000) +``` + +In other words, `"e"` multiplies the number by `1` with the given zeroes count. + +```js +1e3 = 1 * 1000 +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; +``` + +Also the same `"e"` can help. If we'd like not to write down the zeroes explicitly, the same number is: + +```js +let ms = 1e-6; // six zeroes to the left from 1 +``` + +If we count the zeroes in `0.000001`, there are 6 of them. So naturally it's `1e-6`. + +In other words, a negative number after `"e"` means a division by 1 with the given number of zeroes: + +```js +// -3 divides by 1 with 3 zeroes +1e-3 = 1 / 1000 (=0.001) + +// -6 divides by 1 with 6 zeroes +1.23e-6 = 1.23 / 1000000 (=0.00000123) +``` + +### Hex, binary and octal numbers + +[Hexadecimal](https://en.wikipedia.org/wiki/Hexadecimal) numbers are widely used in JavaScript: to represent colors, encode characters and for many other things. So there exists a short way to write them: `0x` and then the number. + +For instance: + +```js run +alert( 0xff ); // 255 +alert( 0xFF ); // 255 (the same, case doesn't matter) +``` + +Binary and octal numeral systems are rarely used, but also supported using `0b` and `0o` prefixes: + + +```js run +let a = 0b11111111; // binary form of 255 +let b = 0o377; // octal form of 255 + +alert( a == b ); // true, the same number 255 at both sides +``` + +There are only 3 numeral systems with such support. For other numeral systems we should use function `parseInt` (later in this chapter). + +## toString(base) + +The method `num.toString(base)` returns a string representation of `num` in the numeral system with the given `base`. + +For example: +```js run +let num = 255; + +alert( num.toString(16) ); // ff +alert( num.toString(2) ); // 11111111 +``` + +The `base` can vary from `2` to `36`. By default it's `10`. + +Most often use cases are: + +- **base=16** is used for hex colors, character encodings etc, digits can be `0..9` or `A..F`. +- **base=2** is mostly for debugging bitwise operations, digits can be `0` or `1`. +- **base=36** is the maximum, digits can be `0..9` or `A..Z`. The whole latin alphabet is used to represent a number. A funny, but useful case for `36` is when we need to turn a long numeric identifier into something shorter, for example to make a short url. Can simply represent it in the numeral system with base `36`: + + ```js run + alert( 123456..toString(36) ); // 2n9c + ``` + +```warn header="Two dots to call a method" +Please note that two dots in `123456..toString(36)` is not a typo. If we want to call a method directly on a number, like `toString` in the example above, then we need to place two dots `..` after it. + +If we placed a single dot: `123456.toString(36)`, then there would be an error, because JavaScript syntax implies the decimal part after the first dot. And if we place one more dot, then JavaScript knows that the decimal part is empty and now goes the method. + +Also could write `(123456).toString(36)`. +``` + +## Rounding + +One of most often operations with numbers is the rounding. + +There are following built-in functions for rounding: + +`Math.floor` +: Rounds down: `3.1` becomes `3`, and `-1.1` becomes `-2`. + +`Math.ceil` +: Rounds up: `3.1` becomes `4`, and `-1.1` becomes `-1`. + +`Math.round` +: Rounds to the nearest integer: `3.1` becomes `3`, `3.6` becomes `4` and `-1.1` becomes `-1`. + +`Math.trunc` (not supported by Internet Explorer) +: Removes the decimal part: `3.1` becomes `3`, `-1.1` becomes `-1`. + +Here's the table to summarize the differences between them: + +| | `Math.floor` | `Math.ceil` | `Math.round` | `Math.trunc` | +|---|---------|--------|---------|---------| +|`3.1`| `3` | `4` | `3` | `3` | +|`3.6`| `3` | `4` | `4` | `3` | +|`-1.1`| `-2` | `-1` | `-1` | `-1` | +|`-1.6`| `-2` | `-1` | `-2` | `-1` | + + +These functions cover all possible ways to deal with the decimal part as a whole. But what if we'd like to round the number to `n-th` digit after the point? + +For instance, we have `1.2345` and want to round it to 2 digits, getting only `1.23`. + +There are two ways to do so. + +1. Multiply-and-divide. + + For instance, to round the number to the 2nd digit after the point, we can multiply the number by `100`, call the rounding function and then divide back. + ```js run + let num = 1.23456; + + alert( Math.floor(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23 + ``` + +2. The method [toFixed(n)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed) rounds the number to `n` digits after the point and returns a string representation of the result. + + ```js run + let num = 12.34; + alert( num.toFixed(1) ); // "12.3" + ``` + + The rounding goes to the nearest value, similar to `Math.round`: + + ```js run + let num = 12.36; + alert( num.toFixed(1) ); // "12.4" + ``` + + Please note that result of `toFixed` is a string. If the decimal part is shorter than required, zeroes are appended to its end: + + ```js run + let num = 12.34; + alert( num.toFixed(5) ); // "12.34000", added zeroes to make exactly 5 digits + ``` + + We can convert it to a number using the unary plus or a `Number()` call: `+num.toFixed(5)`. + +## Imprecise calculations + +Internally, a number is represented in 64-bit format [IEEE-754](http://en.wikipedia.org/wiki/IEEE_754-1985). So, there are exactly 64 bits to store a number: 52 of them are used to store the digits, 11 of them store the position of the decimal point (they are zero for integer numbers) and 1 bit for the sign. + +If a number is too big, it would overflow the 64-bit storage, potentially giving an infinity: + +```js run +alert( 1e500 ); // Infinity +``` + +But what may be a little bit more obvious, but happens much often is the loss of precision. + +Consider this (falsy!) test: + +```js run +alert( 0.1 + 0.2 == 0.3 ); // *!*false*/!* +``` + +Yes, indeed, if we check whether the sum of `0.1` and `0.2` is `0.3`, we get `false`. + +Strange! What is it then if not `0.3`? + +```js run +alert( 0.1 + 0.2 ); // 0.30000000000000004 +``` + +Ouch! There are more consequences than an incorrect comparison here. Imagine you're making an e-shopping site and the visitor puts `$0.10` and `$0.20` goods into his chart. The order total will be `$0.30000000000000004`. That would surprise anyone. + +Why does it work like that? + +A number is stored in memory in it's binary form, as a sequence of ones and zeroes. But fractions like `0.1`, `0.2` that look simple in the decimal numeric system are actually unending fractions in their binary form. + +In other words, what is `0.1`? It is one divided by ten `1/10`, one-tenth. In decimal numeral system such numbers are easily representable. Compare it to one-third: `1/3`. It becomes an endless fraction `0.33333(3)`. + +So, division by powers `10` is guaranteed to look well in the decimal system, but the division by `3` is not. For the same reason, in the binary numeral system, the division by powers of `2` is guaranteed to look good, but `1/10` becomes an endless binary fraction. + +There's just no way to store *exactly 0.1* or *exactly 0.2* in the binary system, just like there is no way to store one-third as a decimal fraction. + +The numeric format IEEE-754 solves that by storing the nearest possible number. There are rounding rules that normally don't allow us to see that "tiny precision loss", so the number shows up as `0.3`. But the loss still exists. + +We can see it like this: +```js run +alert( 0.1.toFixed(20) ); // 0.10000000000000000555 +``` + +And when we sum two numbers, then their "precision losses" sum up. + +That's why `0.1 + 0.2` is not exactly `0.3`. + +```smart header="Not only JavaScript" +The same issue exists in many other programming languages. + +PHP, Java, C, Perl, Ruby give exactly the same result, because they are based on the same numeric format. +``` + +Can we work around the problem? Sure, there's a number of ways: + +1. We can round the result with the help of a method [toFixed(n)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed): + + ```js run + let sum = 0.1 + 0.2; + alert( sum.toFixed(2) ); // 0.30 + ``` + + Please note that `toFixed` always returns a string. It ensures that it has 2 digits after the decimal point. That's actually convenient if we have an e-shopping and need to show `$0.30`. For other cases we can use the unary plus to coerce it into a number: + + ```js run + let sum = 0.1 + 0.2; + alert( +sum.toFixed(2) ); // 0.3 + ``` + +2. We can temporarily turn numbers into integers for the maths and then go back. That would looks like this: + + ```js run + alert( (0.1*10 + 0.2*10) / 10 ); // 0.3 + ``` + + It works, because when we get `0.1*10 = 1` and `0.2 * 10 = 2` then both numbers are integers, there's no precision loss for them. + +3. If it's a shop, then the most radical solution would be to store all prices in cents. No fractions at all. But what if we apply a discount of 30%? In practice, totally evading fractions is rarely feasible, so the solutions listed above are here to help. + +````smart header="The funny thing" +Try running this: + +```js run +// Hello! I'm a self-increasing number! +alert( 9999999999999999 ); // shows 10000000000000000 +``` + +The reason is the same: loss of precision. There are 64 bits for the number, 52 of them can be used to store digits, and that's not enough. So the least significant digits disappear. + +JavaScript doesn't trigger an error in such case. It does the best to fit the number into the format. Unfortunately, the format is not big enough. +```` + +```smart header="Two zeroes" +Another funny consequence of the internal representation is the existance 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 the zero. + +In most cases the distinction is unnoticeable, because operators are suited to treat them as the same. +``` + + + +## Tests: isFinite and isNaN + +Remember the two special numeric values? + +- `Infinite` (and `-Infinite`) is a special numeric value that is greater (less) than anything. +- `NaN` represends an error. + +They belong to the type `number`, but are not "normal" numbers, so there are special functions to check for them: + + +- `isNaN(value)` converts its argument to a number and then tests if for being `NaN`: + + ```js run + alert( isNaN(NaN) ); // true + alert( isNaN("str") ); // true + ``` + + But do we need the function? Can we just use the comparison `=== NaN`? Sorry, but no. The value `NaN` is unique in that it does not equal anything including itself: + + ```js run + alert( NaN === NaN ); // false + ``` + +- `isFinite(value)` converts its argument to a number and returns `true` if it's a regular number, not `NaN/Infinity/-Infinity`: + + ```js run + alert( isFinite("15") ); // true + alert( isFinite("str") ); // false, because a special value: NaN + alert( isFinite(Infinity) ); // false, because a special value: Infinity + ``` + +Sometimes `isFinite` is used to validate the string value for being a regular number: + + +```js run +let num = +prompt("Enter a number", ''); + +// will be true unless you enter Infinity, -Infinity or not a number +alert( isFinite(num) ); +``` + +Please note that an empty or a space-only string is treated as `0` in all numeric functions including `isFinite`. + +```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: + +1. It works with `NaN`: `Object.is(NaN, NaN) === true`, that's a good thing. +2. Values `0` and `-0` are different: `Object.is(0, -0) === false`, it rarely matters, but these values technically are different. + +In all other cases, `Object.is(a, b)` is the same as `a === b`. + +This way of comparison is often used in JavaScript specification. When an internal algorithm needs to compare two values for being exactly the same, it uses `Object.is` (internally called [SameValue](https://tc39.github.io/ecma262/#sec-samevalue)). +``` + + +## parseInt and parseFloat + +The numeric conversion using a plus `+` or `Number()` is strict. If a value is not exactly a number, it fails: + +```js run +alert( +"100px" ); // NaN +``` + +The sole exception is spaces before and after the line, they are ignored. + +But in real life we often have values in units, like `"100px"` or `"12pt"` in CSS. Also in many countries the currency symbol goes after the amount, so we have `"19€"` and would like to extract a numeric value out of that. + +That's what `parseInt` and `parseFloat` are for. + +They "read" a number from a string until they can. In case of an error, the gathered number is returned. Function `parseInt` reads an integer number, `parseFloat` reads any number: + +```js run +alert( parseInt('100px') ); // 100 +alert( parseFloat('12.5em') ); // 12.5 + +alert( parseInt('12.3') ); // 12, only integer part +alert( parseFloat('12.3.4') ); // 12.3, the second point stops the reading +``` + +Of course, there are situations when `parseInt/parseFloat` return `NaN`. It happens when no digits could be read: + +```js run +alert( parseInt('a123') ); // NaN, the first symbol stops he process +``` + +````smart header="The second argument of `parseInt(str, radix)`" +The `parseInt()` function has an optional second parameter. It specifies the base of the numeral system, so `parseInt` can also parse strings of hex numbers, binary numbers and so on: + +```js run +alert( parseInt('0xff', 16) ); // 255 +alert( parseInt('ff', 16) ); // 255, without 0x also works + +alert( parseInt('2n9c', 36) ); // 123456 +``` +```` + +## Other math functions + +JavaScript has a built-in [Math](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Math) object which contains a small library of mathematical functions and constants. + +A few examples: + +`Math.random()` +: Returns a random number from 0 to 1 (not including 1) + + ```js run + alert( Math.random() ); // 0.1234567894322 + alert( Math.random() ); // 0.5435252343232 + alert( Math.random() ); // ... (any random numbers) + ``` + +`Math.max(a, b, c...)` / `Math.min(a, b, c...)` +: Return the greatest/smallest from the arbitrary number of arguments. + + ```js run + alert( Math.max(3, 5, -10, 0, 1) ); // 5 + alert( Math.min(1, 2) ); // 1 + ``` + +`Math.pow(n, power)` +: Returns `n` raised the given power + + ```js run + alert( Math.pow(2, 10) ); // 2 in power 10 = 1024 + ``` + +There are more functions and constants in `Math`, including trigonometry, you can find them in the [docs for the Math](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Math) object. + +## Summary + +To write big numbers: + +- 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. + +For different numeral systems: + +- Can write numbers directly in hex (`0x`), octal (`0o`) and binary (`0b`) systems +- `parseInt(str, base)` parses an integer from any numeral system with base: `2 ≤ base ≤ 36`. +- `num.toString(base)` converts a number to a string in the numeral system with the given `base`. + +For converting values like `12pt` and `100px` to a number: + +- Use `parseInt/parseFloat` for the "soft" conversion, which reads a number from a string until it can. + +For fractions: + +- Round using `Math.floor`, `Math.ceil`, `Math.trunc`, `Math.round` or `num.toFixed(precision)`. +- Remember about the loss of precision when working with fractions. + +More mathematical functions: + +- See the [Math](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Math) object when you need them. The library is very small, but can cover basic needs. + + diff --git a/1-js/4-data-structures/3-string/1-ucfirst/_js.view/solution.js b/1-js/05-data-types/03-string/1-ucfirst/_js.view/solution.js similarity index 100% rename from 1-js/4-data-structures/3-string/1-ucfirst/_js.view/solution.js rename to 1-js/05-data-types/03-string/1-ucfirst/_js.view/solution.js diff --git a/1-js/05-data-types/03-string/1-ucfirst/_js.view/test.js b/1-js/05-data-types/03-string/1-ucfirst/_js.view/test.js new file mode 100644 index 00000000..d5c50ff5 --- /dev/null +++ b/1-js/05-data-types/03-string/1-ucfirst/_js.view/test.js @@ -0,0 +1,9 @@ +describe("ucFirst", function() { + it('Uppercases the first symbol', function() { + assert.strictEqual(ucFirst("john"), "John"); + }); + + it("Doesn't die on an empty string", function() { + assert.strictEqual(ucFirst(""), ""); + }); +}); \ No newline at end of file diff --git a/1-js/05-data-types/03-string/1-ucfirst/solution.md b/1-js/05-data-types/03-string/1-ucfirst/solution.md new file mode 100644 index 00000000..4809cf12 --- /dev/null +++ b/1-js/05-data-types/03-string/1-ucfirst/solution.md @@ -0,0 +1,27 @@ +We can't "replace" the first character, because strings in JavaScript are immutable. + +But we can make a new string based on the existing one, with the uppercased first character: + +```js +let newStr = str[0].toUpperCase() + str.slice(1); +``` + +There's a small problem though. If `str` is empty, then `str[0]` is undefined, so we'll get an error. + +There are two variants here: + +1. Use `str.charAt(0)`, as it always returns a string (maybe empty). +2. Add a test for an empty string. + +Here's the 2nd variant: + +```js run +function ucFirst(str) { + if (!str) return str; + + return str[0].toUpperCase() + str.slice(1); +} + +alert( ucFirst("john") ); // John +``` + diff --git a/1-js/05-data-types/03-string/1-ucfirst/task.md b/1-js/05-data-types/03-string/1-ucfirst/task.md new file mode 100644 index 00000000..c0e6ecac --- /dev/null +++ b/1-js/05-data-types/03-string/1-ucfirst/task.md @@ -0,0 +1,12 @@ +importance: 5 + +--- + +# Uppercast the first character + +Write a function `ucFirst(str)` that returns the string `str` with the uppercased first character, for instance: + +```js +ucFirst("john") == "John"; +``` + diff --git a/1-js/05-data-types/03-string/2-check-spam/_js.view/solution.js b/1-js/05-data-types/03-string/2-check-spam/_js.view/solution.js new file mode 100644 index 00000000..105d70ea --- /dev/null +++ b/1-js/05-data-types/03-string/2-check-spam/_js.view/solution.js @@ -0,0 +1,5 @@ +function checkSpam(str) { + let lowerStr = str.toLowerCase(); + + return lowerStr.includes('viagra') || lowerStr.includes('xxx'); +} \ No newline at end of file diff --git a/1-js/05-data-types/03-string/2-check-spam/_js.view/test.js b/1-js/05-data-types/03-string/2-check-spam/_js.view/test.js new file mode 100644 index 00000000..85eb24fc --- /dev/null +++ b/1-js/05-data-types/03-string/2-check-spam/_js.view/test.js @@ -0,0 +1,13 @@ +describe("checkSpam", function() { + it('finds spam in "buy ViAgRA now"', function() { + assert.isTrue(checkSpam('buy ViAgRA now')); + }); + + it('finds spam in "free xxxxx"', function() { + assert.isTrue(checkSpam('free xxxxx')); + }); + + it('no spam in "innocent rabbit"', function() { + assert.isFalse(checkSpam('innocent rabbit')); + }); +}); \ No newline at end of file diff --git a/1-js/05-data-types/03-string/2-check-spam/solution.md b/1-js/05-data-types/03-string/2-check-spam/solution.md new file mode 100644 index 00000000..2468916f --- /dev/null +++ b/1-js/05-data-types/03-string/2-check-spam/solution.md @@ -0,0 +1,14 @@ +To make the search case-insensitive, let's bring the stirng to lower case and then search: + +```js run +function checkSpam(str) { + let lowerStr = str.toLowerCase(); + + return lowerStr.includes('viagra') || lowerStr.includes('xxx'); +} + +alert( checkSpam('buy ViAgRA now') ); +alert( checkSpam('free xxxxx') ); +alert( checkSpam("innocent rabbit") ); +``` + diff --git a/1-js/05-data-types/03-string/2-check-spam/task.md b/1-js/05-data-types/03-string/2-check-spam/task.md new file mode 100644 index 00000000..d073adc0 --- /dev/null +++ b/1-js/05-data-types/03-string/2-check-spam/task.md @@ -0,0 +1,16 @@ +importance: 5 + +--- + +# Check for spam + +Write a function `checkSpam(str)` that returns `true` if `str` contains 'viagra' or 'XXX', otherwise `false. + +The function must be case-insensitive: + +```js +checkSpam('buy ViAgRA now') == true +checkSpam('free xxxxx') == true +checkSpam("innocent rabbit") == false +``` + diff --git a/1-js/05-data-types/03-string/3-truncate/_js.view/solution.js b/1-js/05-data-types/03-string/3-truncate/_js.view/solution.js new file mode 100644 index 00000000..f587df1f --- /dev/null +++ b/1-js/05-data-types/03-string/3-truncate/_js.view/solution.js @@ -0,0 +1,4 @@ +function truncate(str, maxlength) { + return (str.length > maxlength) ? + str.slice(0, maxlength - 1) + '…' : str; +} \ No newline at end of file diff --git a/1-js/05-data-types/03-string/3-truncate/_js.view/test.js b/1-js/05-data-types/03-string/3-truncate/_js.view/test.js new file mode 100644 index 00000000..c252f16b --- /dev/null +++ b/1-js/05-data-types/03-string/3-truncate/_js.view/test.js @@ -0,0 +1,16 @@ +describe("truncate", function() { + it("truncate the long string to the given lenth (including the ellipsis)", function() { + assert.equal( + truncate("What I'd like to tell on this topic is:", 20), + "What I'd like to te…" + ); + }); + + it("doesn't change short strings", function() { + assert.equal( + truncate("Hi everyone!", 20), + "Hi everyone!" + ); + }); + +}); \ No newline at end of file diff --git a/1-js/05-data-types/03-string/3-truncate/solution.md b/1-js/05-data-types/03-string/3-truncate/solution.md new file mode 100644 index 00000000..a49b7090 --- /dev/null +++ b/1-js/05-data-types/03-string/3-truncate/solution.md @@ -0,0 +1,11 @@ +The maximal length must be `maxlength`, so we need to cut it a little shorter, to give space for the ellipsis. + +Note that there is actually a single unicode character for an ellipsis. That's not three dots. + +```js run +function truncate(str, maxlength) { + return (str.length > maxlength) ? + str.slice(0, maxlength - 1) + '…' : str; +} +``` + diff --git a/1-js/05-data-types/03-string/3-truncate/task.md b/1-js/05-data-types/03-string/3-truncate/task.md new file mode 100644 index 00000000..af37f0dc --- /dev/null +++ b/1-js/05-data-types/03-string/3-truncate/task.md @@ -0,0 +1,17 @@ +importance: 5 + +--- + +# Truncate the text + +Create a function `truncate(str, maxlength)` that checks the length of the `str` and, if it exceeds `maxlength` -- replaces the end of `str` with the ellipsis character `"…"`, to make its length equal to `maxlength`. + +The result of the function should be the truncated (if needed) string. + +For instance: + +```js +truncate("What I'd like to tell on this topic is:", 20) = "What I'd like to te…" + +truncate("Hi everyone!", 20) = "Hi everyone!" +``` diff --git a/1-js/4-data-structures/3-string/4-extract-currency/_js.view/solution.js b/1-js/05-data-types/03-string/4-extract-currency/_js.view/solution.js similarity index 100% rename from 1-js/4-data-structures/3-string/4-extract-currency/_js.view/solution.js rename to 1-js/05-data-types/03-string/4-extract-currency/_js.view/solution.js diff --git a/1-js/05-data-types/03-string/4-extract-currency/_js.view/test.js b/1-js/05-data-types/03-string/4-extract-currency/_js.view/test.js new file mode 100644 index 00000000..1c3f0bbc --- /dev/null +++ b/1-js/05-data-types/03-string/4-extract-currency/_js.view/test.js @@ -0,0 +1,8 @@ +describe("extractCurrencyValue", function() { + + it("for the string $120 returns the number 120", function() { + assert.strictEqual(extractCurrencyValue('$120'), 120); + }); + + +}); \ No newline at end of file diff --git a/2-ui/5-widgets/2-widgets-structure/2-slider-widget/solution.md b/1-js/05-data-types/03-string/4-extract-currency/solution.md similarity index 100% rename from 2-ui/5-widgets/2-widgets-structure/2-slider-widget/solution.md rename to 1-js/05-data-types/03-string/4-extract-currency/solution.md diff --git a/1-js/05-data-types/03-string/4-extract-currency/task.md b/1-js/05-data-types/03-string/4-extract-currency/task.md new file mode 100644 index 00000000..feb16e64 --- /dev/null +++ b/1-js/05-data-types/03-string/4-extract-currency/task.md @@ -0,0 +1,16 @@ +importance: 4 + +--- + +# Extract the money + +We have a cost in the form `"$120"`. That is: the dollar sign goes first, and then the number. + +Create a function `extractCurrencyValue(str)` that would extract the numeric value from such string and return it. + +The example: + +```js +alert( extractCurrencyValue('$120') === 120 ); // true +``` + diff --git a/1-js/05-data-types/03-string/article.md b/1-js/05-data-types/03-string/article.md new file mode 100644 index 00000000..c7d889e9 --- /dev/null +++ b/1-js/05-data-types/03-string/article.md @@ -0,0 +1,676 @@ +# Strings + +In JavaScript, the textual data is stored as strings. There is no separate type for a single character. + +The internal format for strings is always [UTF-16](https://en.wikipedia.org/wiki/UTF-16), it is not tied to the page encoding. + +[cut] + +## Quotes + +Let's remember the kinds of quotes. + +Strings can be enclosed either with the single, double quotes or in backticks: + +```js +let single = 'single-quoted'; +let double = "double-quoted"; + +let backticks = `backticks`; +``` + +Single and double quotes are essentially the same. Backticks allow to embed any expression into the string, including function calls: + +```js run +function sum(a, b) { + return a + b; +} + +alert(`1 + 2 = ${sum(1, 2)}.`); // 1 + 2 = 3. +``` + +Another advantage of using backticks is that they allow a string to span multiple lines: + +```js run +let guestList = `Guests: + * John + * Pete + * Mary +`; + +alert(guestList); // a list of guests, multiple lines +``` + +If we try to use single or double quotes the same way, there will be an error: +```js run +let guestList = "Guests: // Error: Unexpected token ILLEGAL + * John"; +``` + +Single and double quotes come from ancient times of language creation, and the need for multiline strings was not taken into account. Backticks appeared much later and thus are more versatile. + +Backticks also allow to specify a "template function" before the first backtick, the syntax is: func`string`. The function `func` is called automatically, receives the string and embedded expressions and can process them. You can read more in the [docs](mdn:JavaScript/Reference/Template_literals#Tagged_template_literals). That is called "tagged templates". This feature makes it easier to wrap strings into custom templating or other functionality, but is rarely used. + + +## Special characters + +It is still possible to create multiline strings with single quotes, using a so-called "newline character" written as `\n`, that denotes a line break: + +```js run +let guestList = "Guests:\n * John\n * Pete\n * Mary"; + +alert(guestList); // a multiline list of guests +``` + +So to speak, these two lines describe the same: + +```js run +alert( "Hello\nWorld" ); // two lines using a "newline symbol" + +// two lines using a normal newline and backticks +alert( `Hello +World` ); +``` + +There are other, less common "special" characters as well, here's the list: + +| Character | Description | +|-----------|-------------| +|`\b`|Backspace| +|`\f`|Form feed| +|`\n`|New line| +|`\r`|Carriage return| +|`\t`|Tab| +|`\uNNNN`|A unicode symbol with the hex code `NNNN`, for instance `\u00A9` -- is a unicode for the copyright symbol `©`. Must be exactly 4 hex digits. | +|`\u{NNNNNNNN}`|Some rare characters are encoded with two unicode symbols, taking up to 4 bytes. The long unicode requires braces around.| + +Examples with unicode: + +```js run +alert( "\u00A9" ); // © +alert( "\u{20331}" ); // 𠌱, a rare chinese hieroglyph (long unicode) +alert( "\u{1F60D}"); // a smiling face sumbol (another long unicode) +``` + +All special characters start with a backslash character `\`. It is also called an "escaping character". + +We should also use it if we want to insert the quote into the string. + +For instance: + +```js run +alert( 'I*!*\'*/!*m the Walrus!' ); // *!*I'm*/!* the Walrus! +``` + +See, we have to prepend the inner quote by the backslash `\'`, because otherwise it would mean the string end. + +Of course, that refers only for the quotes that are same as the enclosing ones. So, as a more elegant solution, we could switch to double quotes or backticks instead: + +```js run +alert( `I'm the Walrus!` ); // I'm the Walrus! +``` + +Note that the backslash `\` serves for the correct reading of the string by JavaScript, then disappears. The in-memory string has no `\`. You can clearly see that in `alert` from the examples above. + +But what if we need exactly a backslash `\` in the string? + +That's possible, but we need to double it like `\\`: + +```js run +alert( `The backslash: \\` ); // The backslash: \ +``` + +## String length + + +The `length` property has the string length: + +```js run +alert( `My\n`.length ); // 3 +``` + +Note that `\n` is a single "special" character, so the length is indeed `3`. + +```warn header="`length` is a property" +People with background in some other languages sometimes mistype by calling `str.length()` instead of just `str.length`. That doesn't work. + +Please note that `str.length` is a numeric property, not a function. There is no need to add brackets after it. +``` + +## Accessing characters + +To get a character at position `pos`, use square brackets `[pos]` or call the method [str.charAt(pos)](mdn:js/String/charAt). The first character starts from the zero position: + +```js run +let str = `Hello`; + +// the first character +alert( str[0] ); // H +alert( str.charAt(0) ); // H + +// the last character +alert( str[str.length - 1] ); // o +``` + +The square brackets is a modern way of getting a character, while `charAt` exists mostly for historical reasons. + +The only difference between them is that if no character found, `[]` returns `undefined`, and `charAt` returns an empty string: + +```js run +let str = `Hello`; + +alert( str[1000] ); // undefined +alert( str.charAt(1000) ); // '' (an empty string) +``` + +Also we can iterate over characters using `for..of`: + +```js run +for(let char of "Hello") { + alert(char); // H,e,l,l,o (char becomes "H", then "e", then "l" etc) +} +``` + +## Strings are immutable + +Strings can't be changed in JavaScript. It is impossible to change a character. + +Let's try to see that it doesn't work: + +```js run +let str = 'Hi'; + +str[0] = 'h'; // error +alert( str[0] ); // doesn't work +``` + +The usual workaround is to create a whole new string and assign it to `str` instead of the old one. + +For instance: + +```js run +let str = 'Hi'; + +str = 'h' + str[1]; // replace the string + +alert( str ); // hi +``` + +In the following sections we'll see more examples of that. + +## Changing the case + +Methods [toLowerCase()](mdn:js/String/toLowerCase) and [toUpperCase()](mdn:js/String/toUpperCase) change the case: + +```js run +alert( 'Interface'.toUpperCase() ); // INTERFACE +alert( 'Interface'.toLowerCase() ); // interface +``` + +Or, if we want a single character lowercased: + +```js +alert( 'Interface'[0].toLowerCase() ); // 'i' +``` + +## Searching for a substring + +There are multiple ways to look for a substring in a string. + +### str.indexOf + +The first method is [str.indexOf(substr, pos)](mdn:js/String/indexOf). + +It looks for the `substr` in `str`, starting from the given position `pos`, and returns the position where the match was found or `-1` if nothing can be found. + +For instance: + +```js run +let str = 'Widget with id'; + +alert( str.indexOf('Widget') ); // 0, because 'Widget' is found at the beginning +alert( str.indexOf('widget') ); // -1, not found, the search is case-sensitive + +alert( str.indexOf("id") ); // 1, "id" is found at the position 1 (..idget with id) +``` + +The optional second parameter allows to search starting from the given position. + +For instance, the first occurence of `"id"` is at the position `1`. To look for the next occurence, let's start the search from the position `2`: + +```js run +let str = 'Widget with id'; + +alert( str.indexOf('id', 2) ) // 12 +``` + + +If we're interested in all occurences, we can run `indexOf` in a loop. Every new call is made with the position after the previous match: + + +```js run +let str = 'As sly as a fox, as strong as an ox'; + +let target = 'as'; // let's look for it + +let pos = 0; +while (true) { + let foundPos = str.indexOf(target, pos); + if (foundPos == -1) break; + + alert( `Found at ${foundPos}` ); + pos = foundPos + 1; // continue the search from the next position +} +``` + +The same algorithm can be layed out shorter: + +```js run +let str = "As sly as a fox, as strong as an ox"; +let target = "as"; + +*!* +let pos = -1; +while ((pos = str.indexOf(target, pos + 1)) != -1) { + alert( pos ); +} +*/!* +``` + +```smart header="`str.lastIndexOf(pos)`" +There is also a similar method [str.lastIndexOf(pos)](mdn:js/String/lastIndexOf) that searches from the end of the string to its beginning. + +It would list the occurences in the reverse way. +``` + +There is a slight inconvenience with `indexOf` in the `if` test. We can't put it in the `if` like this: + +```js run +let str = "Widget with id"; + +if (str.indexOf("Widget")) { + alert("We found it"); // doesn't work! +} +``` + +The `alert` in the example above doesn't show, because `str.indexOf("Widget")` returns `0` (meaning that it found the match at the starting position). Right, but `if` considers that to be `false`. + +So, we should actualy check for `-1`, like that: + +```js run +let str = "Widget with id"; + +*!* +if (str.indexOf("Widget") != -1) { +*/!* + alert("We found it"); // works now! +} +``` + +````smart header="The bitwise NOT trick" +One of the old tricks used here is the [bitwise NOT](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Bitwise_NOT) `~` operator. It converts the number to 32-bit integer (removes the decimal part if exists) and then reverses all bits in its binary representation. + +For 32-bit integers the call `~n` means exactly the same as `-(n+1)` (due to IEEE-754 format). + +For instance: + +```js run +alert( ~2 ); // -3, the same as -(2+1) +alert( ~1 ); // -2, the same as -(1+1) +alert( ~0 ); // -1, the same as -(0+1) +*!* +alert( ~-1 ); // 0, the same as -(-1+1) +*/!* +``` + +As we can see, `~n` is zero only if `n == -1`. + +So, the test `if ( ~str.indexOf("...") )` is truthy that the result of `indexOf` is not `-1`. In other words, when there is a match. + +People use it to shorten `indexOf` checks: + +```js run +let str = "Widget"; + +if (~str.indexOf("Widget")) { + alert( 'Found it!' ); // works +} +``` + +It is usually not recommended to use language features in a non-obvious way, but this particular trick is widely used in old code, so we should understand it. + +Just remember: `if (~str.indexOf(...))` reads as "if found". +```` + +### includes, startsWith, endsWith + +The more modern method [str.includes(substr, pos)](mdn:js/String/includes) returns `true/false` depending on whether `str` has `substr` as its part. + +It's the right choice if we need to test for the match, but don't need its position: + +```js run +alert( "Widget with id".includes("Widget") ); // true + +alert( "Hello".includes("Bye") ); // false +``` + +The optional second argument of `str.includes` is the position to start searching from: + +```js run +alert( "Midget".includes("id") ); // true +alert( "Midget".includes("id", 3) ); // false, from position 3 there is no "id" +``` + +The methods [str.startsWith](mdn:js/String/startsWith) and [str.endsWith](mdn:js/String/endsWith) do exactly what they say: + +```js run +alert( "Widget".startsWith("Wid") ); // true, "Widget" starts with "Wid" +alert( "Widget".endsWith("get") ); // true, "Widget" ends with "get" +``` + +## Getting a substring + +There are 3 methods in JavaScript to get a substring: `substring`, `substr` and `slice`. + +`str.slice(start [, end])` +: Returns the part of the string from `start` to (but not including) `end`. + + For instance: + + ```js run + let str = "stringify"; + alert( str.slice(0,5) ); // 'strin', the substring from 0 to 5 (not including 5) + alert( str.slice(0,1) ); // 's', from 0 to 1, but not including 1, so only character at 0 + ``` + + If there is no second argument, then `slice` goes till the end of the string: + + ```js run + let str = "st*!*ringify*/!*"; + alert( str.slice(2) ); // ringify, from the 2nd position till the end + ``` + + Negative values for `start/end` are also possible. They mean the position is counted from the string end: + + ```js run + let str = "strin*!*gif*/!*y"; + + // start at the 4th position from the right, end at the 1st from the right + alert( str.slice(-4, -1) ); // gif + ``` + + +`str.substring(start [, end])` +: Returns the part of the string *between* `start` and `end`. + + Almost the same as `slice`, but allows `start` to be greater than `end`. + + For instance: + + + ```js run + let str = "st*!*ring*/!*ify"; + + // these are same for substring + alert( str.substring(2, 6) ); // "ring" + alert( str.substring(6, 2) ); // "ring" + + // ...but not for slice: + alert( str.slice(2, 6) ); // "ring" (the same) + alert( str.slice(6, 2) ); // "" (an empty string) + + ``` + + Negative arguments are (unlike slice) not supported, they are treated as `0`. + + +`str.substr(start [, length])` +: Returns the part of the string from `start`, with the given `length`. + + In contrast with the previous methods, this one allows to specify the `length` instead of the ending position: + + ```js run + let str = "st*!*ring*/!*ify"; + alert( str.substr(2, 4) ); // ring, from the 2nd position get 4 characters + ``` + + The first argument may be negative, to count from the end: + + ```js run + let str = "strin*!*gi*/!*fy"; + alert( str.substr(-4, 2) ); // gi, from the 4th position get 2 characters + ``` + +Let's recap the methods to avoid any confusion: + +| method | selects... | negatives | +|--------|-----------|-----------| +| `slice(start, end)` | from `start` to `end` | allows negatives | +| `substring(start, end)` | between `start` and `end` | negative values mean `0` | +| `substr(start, length)` | from `start` get `length` characters | allows negative `start` | + + +```smart header="Which one to choose?" +All of them can do the job. Formally, `substr` has a minor drawback: it is described not in the core JavaScript specification, but in Annex B, which covers browser-only features that exist mainly for historical reasons. So, non-browser environments may fail to support it. But in practice it works everywhere. + +The author finds himself using `slice` almost all the time. +``` + +## Comparing strings + +As we know from the chapter , strings are compared character-by-character, in the alphabet order. + +Although, there are some oddities. + +1. A lowercase letter is always greater than the uppercase: + + ```js run + alert( 'a' > 'Z' ); // true + ``` + +2. Letters with diacritical marks are "out of order": + + ```js run + alert( 'Österreich' > 'Zealand' ); // true + ``` + + That may lead to strange results if we sort country names. Usually people would await for `Zealand` to be after `Österreich` in the list. + +To understand what happens, let's review the internal representation of strings in JavaScript. + +All strings are encoded using [UTF-16](https://en.wikipedia.org/wiki/UTF-16). That is: each character has a corresponding numeric code. There are special methods that allow to get the character for the code and back. + +`str.codePointAt(pos)` +: Returns the code for the character at position `pos`: + + ```js run + // different case letters have different codes + alert( "z".codePointAt(0) ); // 122 + alert( "Z".codePointAt(0) ); // 90 + ``` + +`String.fromCodePoint(code)` +: Creates a character by its numeric `code` + + ```js run + alert( String.fromCodePoint(90) ); // Z + ``` + + We can also add unicode characters by their codes using `\u` followed by the hex code: + + ```js run + // 90 is 5a in hexadecimal system + alert( '\u005a' ); // Z + ``` + +Now let's see the characters with codes `65..220` (the latin alphabet and a little bit extra) by making a string of them: + +```js run +let str = ''; + +for (let i = 65; i <= 220; i++) { + str += String.fromCodePoint(i); +} +alert( str ); +// ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~€‚ƒ„ +// ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜ +``` + +See? Capital character go first, then few special ones, then lowercase characters. + +Now it becomes obvious why `a > Z`. + +The characters are compared by their numeric code. The greater code means that the character is greater. The code for `a` (97) is greater than the code for `Z` (90). + +- All lowercase letters go after uppercase letters, their codes are greater. +- Some letters like `Ö` stand apart from the main alphabet. Here, it's code is greater than anything from `a` to `z`. + + +### Correct comparisons + +The "right" algorithm to do string comparisons is more complex than it may seem. Because alphabets are different for different languages. The same-looking letter may be located differently in different alphabets. + +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). + +It provides a special method to compare strings in different languages, following their rules. + +The call [str.localeCompare(str2)](mdn:js/String/localeCompare): + +- Returns `1` if `str` is greater than `str2` according to the language rules. +- Returns `-1` if `str` is less than `str2`. +- Returns `0` if they are equal. + +For instance: + +```js run +alert( 'Österreich'.localeCompare('Zealand') ); // -1 +``` + +The method actually has two additional arguments specified in [the documentation](mdn:js/String/localeCompare), that allow to specify the language (by default taken from the environment) and setup additional rules like case sensivity or should `"a"` and `"á"` be treated as the same etc. + +## Internals, Unicode + +```warn header="Advanced knowledge" +The section goes deeper into string internals. The knowledge will be useful for you if you plan to deal with emoji, rare mathematical of hieroglyphs characters or other rare symbols. + +You can skip the section if you don't plan to support them. +``` + +### Surrogate pairs + +Most symbols have a 2-byte code. Letters of most european languages, numbers, even most hieroglyphs have a 2-byte representation. + +But 2 bytes only allow 65536 combinations that's not enough for every possible symbol. So rare symbols are encoded with a pair of 2-byte characters called "a surrogate pair". + +The length of such symbols is `2`: + +```js run +alert( '𝒳'.length ); // 2, MATHEMATICAL SCRIPT CAPITAL X +alert( '😂'.length ); // 2, FACE WITH TEARS OF JOY +alert( '𩷶'.length ); // 2, a rare chinese hieroglyph +``` + +Note that surrogate pairs did not exist at the time when JavaScript was created, and thus are not correctly processed by the language! + +We actually have a single symbol in each of the strings above, but the `length` shows the length of `2`. + +`String.fromCodePoint` and `str.codePointAt` are few rare methods that deal with surrogate pairs right. They recently appeared in the language. Before them, there were only [String.fromCharCode](mdn:js/String/fromCharCode) and [str.charCodeAt](mdn:js/String/charCodeAt). These methods are actually the same as `fromCodePoint/codePointAt`, but don't work with surrogate pairs. + +But, for instance, getting a symbol can be tricky, because surrogate pairs are treated as two characters: + +```js run +alert( '𝒳'[0] ); // strange symbols... +alert( '𝒳'[1] ); // ...pieces of the surrogate pair +``` + +Note that pieces of the surrogate pair have no meaning without each other. So, the alerts in the example above actually display garbage. + +Technically, surrogate pairs are also detectable by their codes: if a character has the code in the interval of `0xd800..0xdbff`, then it is the first part of the surrogate pair. The next character (second part) must have the code in interval `0xdc00..0xdfff`. These intervals are reserved exclusively for surrogate pairs by the standard. + +In the case above: + +```js run +// charCodeAt is not surrogate-pair aware, so it gives codes for parts + +alert( '𝒳'.charCodeAt(0).toString(16) ); // d835, between 0xd800 and 0xdbff +alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3, between 0xdc00 and 0xdfff +``` + +You will find more ways to deal with surrogate pairs later in the chapter . Probably, there are special libraries for that too, but nothing famous enough to suggest here. + +### Diacritical marks and normalization + +In many languages there are symbols that are composed of the base character and a mark above/under it. + +For instance, letter `a` can be the base character for: `àáâäãåā`. Most common "composite" character have their own code in the UTF-16 table. But not all of them, because there are too many possible combinations. + +To support arbitrary compositions, UTF-16 allows to use several unicode characters. The base character and one or many "mark" characters that "decorate" it. + +For instance, if we have `S` followed by the special "dot above" character (code `\u0307`), it is shown as Ṡ. + +```js run +alert( 'S\u0307' ); // Ṡ +``` + +If we need a one more mark over the letter (or below it) -- no problem, just add the necessary mark character. + +For instance, if we append a character "dot below" (code `\u0323`), then we'll have "S with dots above and below": `Ṩ`. + +The example: + +```js run +alert( 'S\u0307\u0323' ); // Ṩ +``` + +This gives great flexibility, but also an interesting problem: the same symbol visually can be represented with different unicode compositions. + +For instance: + +```js run +alert( 'S\u0307\u0323' ); // Ṩ, S + dot above + dot below +alert( 'S\u0323\u0307' ); // Ṩ, S + dot below + dot above + +alert( 'S\u0307\u0323' == 'S\u0323\u0307' ); // false +``` + +To solve it, there exists a "unicode normalization" algorithm that brings each string to the single "normal" form. + +It is implemented by [str.normalize()](mdn:js/String/normalize). + +```js run +alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true +``` + +It's funny that in our situation `normalize()` actually brings a sequence of 3 characters to one: `\u1e68` (S with two dots). + +```js run +alert( "S\u0307\u0323".normalize().length ); // 1 + +alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true +``` + +In real, that is not always so. The reason is that symbol `Ṩ` is "common enough", so UTF-16 creators included it into the main table and gave it the code. + +If you want to learn more about normalization rules and variants -- they are described in the appendix to the Unicode standard: [Unicode Normalization Forms](http://www.unicode.org/reports/tr15/), but for most practical reasons the information from this section is enough. + + +## Summary + +- There are 3 types of quotes. Backticks allow a string to span multiple lines and embed expressions. +- Strings in JavaScript are encoded using UTF-16. +- We can use special characters like `\n` and insert letters by their unicode using `\u...`. +- To get a character: use `[]`. +- To get a substring: use `slice` or `substring`. +- To lowercase/uppercase a string: use `toLowerCase/toUpperCase`. +- To look for a substring: use `indexOf`, or `includes/startsWith/endsWith` for simple checks. +- To compare strings according to the language, use `localeCompare`, otherwise they are compared by character codes. + +There are several other helpful methods in strings: + +- `str.trim()` -- removes ("trims") spaces from the beginning and end of the string. +- `str.repeat(n)` -- repeats the string `n` times. +- ...and others, see the [manual](mdn:js/String) for details. + +Also strings have methods for doing search/replace with regular expressions. But that topic deserves a separate chapter, so we'll return to that later. diff --git a/1-js/05-data-types/04-array/1-item-value/solution.md b/1-js/05-data-types/04-array/1-item-value/solution.md new file mode 100644 index 00000000..e631f1c7 --- /dev/null +++ b/1-js/05-data-types/04-array/1-item-value/solution.md @@ -0,0 +1,17 @@ +The result is `4`: + + +```js run +let fruits = ["Apples", "Pear", "Orange"]; + +let shoppingCart = fruits; + +shoppingCart.push("Banana"); + +*!* +alert( fruits.length ); // 4 +*/!* +``` + +That's because arrays are objects. So both `shoppingCart` and `fruits` are the references to the same array. + diff --git a/1-js/05-data-types/04-array/1-item-value/task.md b/1-js/05-data-types/04-array/1-item-value/task.md new file mode 100644 index 00000000..4fcf384f --- /dev/null +++ b/1-js/05-data-types/04-array/1-item-value/task.md @@ -0,0 +1,19 @@ +importance: 3 + +--- + +# Is array copied? + +What is this code going to show? + +```js +let fruits = ["Apples", "Pear", "Orange"]; + +// push a new value into the "copy" +let shoppingCart = fruits; +shoppingCart.push("Banana"); + +// what's in fruits? +alert( fruits.length ); // ? +``` + diff --git a/1-js/05-data-types/04-array/10-maximal-subarray/_js.view/solution.js b/1-js/05-data-types/04-array/10-maximal-subarray/_js.view/solution.js new file mode 100644 index 00000000..51632837 --- /dev/null +++ b/1-js/05-data-types/04-array/10-maximal-subarray/_js.view/solution.js @@ -0,0 +1,11 @@ +function getMaxSubSum(arr) { + let maxSum = 0; + let partialSum = 0; + + for (let item of arr) { + partialSum += item; + maxSum = Math.max(maxSum, partialSum); + if (partialSum < 0) partialSum = 0; + } + return maxSum; +} \ No newline at end of file diff --git a/1-js/05-data-types/04-array/10-maximal-subarray/_js.view/test.js b/1-js/05-data-types/04-array/10-maximal-subarray/_js.view/test.js new file mode 100644 index 00000000..143ad534 --- /dev/null +++ b/1-js/05-data-types/04-array/10-maximal-subarray/_js.view/test.js @@ -0,0 +1,33 @@ +describe("getMaxSubSum", function() { + it("maximal subsum of [1, 2, 3] equals 6", function() { + assert.equal(getMaxSubSum([1, 2, 3]), 6); + }); + + it("maximal subsum of [-1, 2, 3, -9] equals 5", function() { + assert.equal(getMaxSubSum([-1, 2, 3, -9]), 5); + }); + + it("maximal subsum of [-1, 2, 3, -9, 11] equals 11", function() { + assert.equal(getMaxSubSum([-1, 2, 3, -9, 11]), 11); + }); + + it("maximal subsum of [-2, -1, 1, 2] equals 3", function() { + assert.equal(getMaxSubSum([-2, -1, 1, 2]), 3); + }); + + it("maximal subsum of [100, -9, 2, -3, 5] equals 100", function() { + assert.equal(getMaxSubSum([100, -9, 2, -3, 5]), 100); + }); + + it("maximal subsum of [] equals 0", function() { + assert.equal(getMaxSubSum([]), 0); + }); + + it("maximal subsum of [-1] equals 0", function() { + assert.equal(getMaxSubSum([-1]), 0); + }); + + it("maximal subsum of [-1, -2] equals 0", function() { + assert.equal(getMaxSubSum([-1, -2]), 0); + }); +}); \ No newline at end of file diff --git a/1-js/05-data-types/04-array/10-maximal-subarray/solution.md b/1-js/05-data-types/04-array/10-maximal-subarray/solution.md new file mode 100644 index 00000000..a2ba50bf --- /dev/null +++ b/1-js/05-data-types/04-array/10-maximal-subarray/solution.md @@ -0,0 +1,94 @@ +# The slow solution + +We can calculate all possible subsums. + +The simplest way is to take every element and calculate sums of all subarrays starting from it. + +For instance, for `[-1, 2, 3, -9, 11]`: + +```js no-beautify +// Starting from -1: +-1 +-1 + 2 +-1 + 2 + 3 +-1 + 2 + 3 + (-9) +-1 + 2 + 3 + (-9) + 11 + +// Starting from 2: +2 +2 + 3 +2 + 3 + (-9) +2 + 3 + (-9) + 11 + +// Starting from 3: +3 +3 + (-9) +3 + (-9) + 11 + +// Starting from -9 +-9 +-9 + 11 + +// Starting from -11 +-11 +``` + +The code is actually a nested loop: the external loop over array elements, and the internal counts subsums starting with the current element. + +```js run +function getMaxSubSum(arr) { + let maxSum = 0; // if we take no elements, zero will be returned + + for (let i = 0; i < arr.length; i++) { + let sumFixedStart = 0; + for (let j = i; j < arr.length; j++) { + sumFixedStart += arr[j]; + maxSum = Math.max(maxSum, sumFixedStart); + } + } + + return maxSum; +} + +alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 +alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 +alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 +alert( getMaxSubSum([1, 2, 3]) ); // 6 +alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 +``` + +The solution has a time complexety of [O(n2)](https://en.wikipedia.org/wiki/Big_O_notation). In other words, if we increase the array size 2 times, the algorithm will work 4 times longer. + +For big arrays (1000, 10000 or more items) such algorithms can lead to a seroius sluggishness. + +# Fast solution + +Let's walk the array and keep the current partial sum of elements in the variable `s`. If `s` becomes negative at some point, then assign `s=0`. The maximum of all such `s` will be the answer. + +If the description is too vague, please see the code, it's short enough: + +```js run +function getMaxSubSum(arr) { + let maxSum = 0; + let partialSum = 0; + + for (let item of arr) { // for each item of arr + partialSum += item; // add it to partialSum + maxSum = Math.max(maxSum, partialSum); // remember the maximum + if (partialSum < 0) partialSum = 0; // zero if negative + } + + return maxSum; +} + +alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 +alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 +alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 +alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 +alert( getMaxSubSum([1, 2, 3]) ); // 6 +alert( getMaxSubSum([-1, -2, -3]) ); // 0 +``` + +The algorithm requires exactly 1 array pass, so the time complexity is O(n). + +You can find more detail information about the algorithm here: [Maximum subarray problem](http://en.wikipedia.org/wiki/Maximum_subarray_problem). If it's still not obvious why that works, then please trace the algorithm on the examples above, see how it works, that's better than any words. 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 new file mode 100644 index 00000000..b5ffc4c2 --- /dev/null +++ b/1-js/05-data-types/04-array/10-maximal-subarray/task.md @@ -0,0 +1,30 @@ +importance: 2 + +--- + +# A maximal subarray + +The input is an array of numbers, e.g. `arr = [1, -2, 3, 4, -9, 6]`. + +The task is: find the contiguous subarray of `arr` with the maximal sum of items. + +Write the function `getMaxSubSum(arr)` that will find return that sum. + +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) +``` + +If all items are negative, it means that we take none (the subarray is empty), so the sum is zero: + +```js +getMaxSubSum([-1, -2, -3]) = 0 +``` + +Please try to think of a fast solution: [O(n2)](https://en.wikipedia.org/wiki/Big_O_notation) or even O(n) if you can. \ No newline at end of file diff --git a/1-js/05-data-types/04-array/2-create-array/solution.md b/1-js/05-data-types/04-array/2-create-array/solution.md new file mode 100644 index 00000000..24351434 --- /dev/null +++ b/1-js/05-data-types/04-array/2-create-array/solution.md @@ -0,0 +1,10 @@ + + +```js run +let styles = ["Jazz", "Blues"]; +styles.push("Rock-n-Roll"); +styles[(styles.length + 1) / 2] = "Classics"; +alert( styles.shift() ); +styles.unshift("Rap", "Reggie"); +``` + diff --git a/1-js/05-data-types/04-array/2-create-array/task.md b/1-js/05-data-types/04-array/2-create-array/task.md new file mode 100644 index 00000000..e6dab6a9 --- /dev/null +++ b/1-js/05-data-types/04-array/2-create-array/task.md @@ -0,0 +1,24 @@ +importance: 5 + +--- + +# Array operations. + +Let's try 5 array operations. + +1. Create an array `styles` with items "Jazz" and "Blues". +2. Append "Rock-n-Roll" to the end. +3. Replace the value in the middle by "Classics". Your code for finding the middle value should work for any arrays with odd length. +4. Strip off the first value of the array and show it. +5. Prepend `Rap` and `Reggie` to the array. + +The array in the process: + +```js no-beautify +Jazz, Blues +Jazz, Bues, Rock-n-Roll +Jazz, Classics, Rock-n-Roll +Classics, Rock-n-Roll +Rap, Reggie, Classics, Rock-n-Roll +``` + diff --git a/1-js/05-data-types/04-array/3-call-array-this/solution.md b/1-js/05-data-types/04-array/3-call-array-this/solution.md new file mode 100644 index 00000000..e994ae07 --- /dev/null +++ b/1-js/05-data-types/04-array/3-call-array-this/solution.md @@ -0,0 +1,15 @@ +The call `arr[2]()` is syntactically the good old `obj[method]()`, in the role of `obj` we have `arr`, and in the role of `method` we have `2`. + +So we have a call of the function `arr[2]` as an object method. Naturally, it receives `this` referencing the object `arr` and outputs the array: + +```js run +let arr = ["a", "b"]; + +arr.push(function() { + alert( this ); +}) + +arr[2](); // "a","b",function +``` + +The array has 3 values: initially it had two, plus the function. diff --git a/1-js/05-data-types/04-array/3-call-array-this/task.md b/1-js/05-data-types/04-array/3-call-array-this/task.md new file mode 100644 index 00000000..340c5fee --- /dev/null +++ b/1-js/05-data-types/04-array/3-call-array-this/task.md @@ -0,0 +1,18 @@ +importance: 5 + +--- + +# Calling in an array context + +What is the result? Why? + +```js +let arr = ["a", "b"]; + +arr.push(function() { + alert( this ); +}) + +arr[2](); // ? +``` + diff --git a/1-js/05-data-types/04-array/5-array-input-sum/solution.md b/1-js/05-data-types/04-array/5-array-input-sum/solution.md new file mode 100644 index 00000000..cbdfbded --- /dev/null +++ b/1-js/05-data-types/04-array/5-array-input-sum/solution.md @@ -0,0 +1,27 @@ +Please note the subtle, but important detail of the solution. We don't convert `value` to number instantly after `prompt`, because after `value = +value` we would not be able to tell an empty string (stop sign) from the zero (valid number). We do it later instead. + + +```js run demo +function sumInput() { + + let numbers = []; + + while (true) { + + let value = prompt("A number please?", 0); + + // should we cancel? + if (value === "" || value === null || !isFinite(value)) break; + + numbers.push(+value); + } + + let sum = 0; + for (let number of numbers) { + sum += number; + } +} + +alert( sumInput() ); +``` + diff --git a/1-js/05-data-types/04-array/5-array-input-sum/task.md b/1-js/05-data-types/04-array/5-array-input-sum/task.md new file mode 100644 index 00000000..4af8e7c9 --- /dev/null +++ b/1-js/05-data-types/04-array/5-array-input-sum/task.md @@ -0,0 +1,15 @@ +importance: 4 + +--- + +# Sum input numbers + +Write the function `sumInput()` that: + +- Asks the user for values using `prompt` and stores the values in the array. +- Finishes asking when the user enters a non-numeric value, an empty string, or presses "Cancel". +- Calculates and returns the sum of array items. + +P.S. A zero `0` is a valid number, please don't stop the input on zero. + +[demo] diff --git a/1-js/05-data-types/04-array/array-pop.png b/1-js/05-data-types/04-array/array-pop.png new file mode 100644 index 00000000..023642fa Binary files /dev/null and b/1-js/05-data-types/04-array/array-pop.png differ diff --git a/1-js/05-data-types/04-array/array-pop@2x.png b/1-js/05-data-types/04-array/array-pop@2x.png new file mode 100644 index 00000000..301fd8f6 Binary files /dev/null and b/1-js/05-data-types/04-array/array-pop@2x.png differ diff --git a/1-js/05-data-types/04-array/array-shift.png b/1-js/05-data-types/04-array/array-shift.png new file mode 100644 index 00000000..5f2cef5d Binary files /dev/null and b/1-js/05-data-types/04-array/array-shift.png differ diff --git a/1-js/05-data-types/04-array/array-shift@2x.png b/1-js/05-data-types/04-array/array-shift@2x.png new file mode 100644 index 00000000..1bd68b31 Binary files /dev/null and b/1-js/05-data-types/04-array/array-shift@2x.png differ diff --git a/1-js/05-data-types/04-array/array-speed.png b/1-js/05-data-types/04-array/array-speed.png new file mode 100644 index 00000000..3737e824 Binary files /dev/null and b/1-js/05-data-types/04-array/array-speed.png differ diff --git a/1-js/05-data-types/04-array/array-speed@2x.png b/1-js/05-data-types/04-array/array-speed@2x.png new file mode 100644 index 00000000..e45624b5 Binary files /dev/null and b/1-js/05-data-types/04-array/array-speed@2x.png differ diff --git a/1-js/05-data-types/04-array/article.md b/1-js/05-data-types/04-array/article.md new file mode 100644 index 00000000..bd7a64e6 --- /dev/null +++ b/1-js/05-data-types/04-array/article.md @@ -0,0 +1,466 @@ +# Arrays + +Objects allow to store keyed collections of values. That's fine. + +But quite often we find that we need an *ordered collection*, where we have a 1st, a 2nd, a 3rd element and so on. For example, we need that to store a list of something: users, goods, HTML elements etc. + +It not convenient to use an object here, because it provides no methods to manage the order of elements. We can’t insert a new property “between” the existing ones. Objects are just not meant for such use. + +There exists a special data structure named `Array`, to store ordered collections. + +[cut] + +## Declaration + +There are two syntaxes for creating an empty array: + +```js +let arr = new Array(); +let arr = []; +``` + +Almost all the time, the second syntax is used. We can supply initial elements in the brackets: + +```js +let fruits = ["Apple", "Orange", "Plum"]; +``` + +Array elements are numbered, starting with zero. + +We can get an element by its number in square brackets: + +```js run +let fruits = ["Apple", "Orange", "Plum"]; + +alert( fruits[0] ); // Apple +alert( fruits[1] ); // Orange +alert( fruits[2] ); // Plum +``` + +We can replace an element: + +```js +fruits[2] = 'Pear'; // now ["Apple", "Orange", "Pear"] +``` + +...Or add a new one to the array: + +```js +fruits[3] = 'Lemon'; // now ["Apple", "Orange", "Pear", "Lemon"] +``` + +The total count of the elements in the array is its `length`: + +```js run +let fruits = ["Apple", "Orange", "Plum"]; + +alert( fruits.length ); // 3 +``` + +We can also use `alert` to show the whole array. + +```js run +let fruits = ["Apple", "Orange", "Plum"]; + +alert( fruits ); // Apple,Orange,Plum +``` + +An array can store elements of any type. + +For instance: + +```js run no-beautify +// mix of values +let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ]; + +// get the object at index 1 and then show its name +alert( arr[1].name ); // John + +// get the function at index 3 and run it +arr[3](); // hello +``` + + +````smart header="Trailing comma" +An array, just like an object, may end with a comma: +```js +let fruits = [ + "Apple", + "Orange", + "Plum"*!*,*/!* +]; +``` + +The "trailing comma" style makes it easier to insert/remove items, because all lines become alike. +```` + + +## Methods pop/push, shift/unshift + +A [queue](https://en.wikipedia.org/wiki/Queue_(abstract_data_type)) is one of most common uses of an array. In computer science, this means an ordered collection of elements which supports two operations: + +- `push` appends an element to the end. +- `shift` get an element from the beginning, advancing the queue, so that the 2nd element becomes the 1st. + +![](queue.png) + +Arrays support both operations. + +In practice we meet it very often. For example, a queue of messages that need to be shown on-screen. + +There's another use case for arrays -- the data structure named [stack](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)). + +It supports two operations: + +- `push` adds an element to the end. +- `pop` takes an element to the end. + +So new elements are added or taken always from the "end". + +A stack is usually illustrated as a pack of cards: new cards are added to the top or taken from the top: + +![](stack.png) + +For stacks, the latest pushed item is received first, that's also called LIFO (Last-In-First-Out) principle. For queues, we have FIFO (First-In-First-Out). + +Arrays in JavaScript can work both as a queue and as a stack. They allow to add/remove elements both to/from the beginning or the end. + +In computer science the data structure that allows it is called [deque](https://en.wikipedia.org/wiki/Double-ended_queue). + +**Methods that work with the end of the array:** + +`pop` +: Extracts the last element of the array and returns it: + + ```js run + let fruits = ["Apple", "Orange", "Pear"]; + + alert( fruits.pop() ); // remove "Pear" and alert it + + alert( fruits ); // Apple, Orange + ``` + +`push` +: Append the element to the end of the array: + + ```js run + let fruits = ["Apple", "Orange"]; + + fruits.push("Pear"); + + alert( fruits ); // Apple, Orange, Pear + ``` + + The call `fruits.push(...)` is equal to `fruits[fruits.length] = ...`. + +**Methods that work with the beginning of the array:** + +`shift` +: Extracts the first element of the array and returns it: + + ```js + let fruits = ["Apple", "Orange", "Pear"]; + + alert( fruits.shift() ); // remove Apple and alert it + + alert( fruits ); // Orange, Pear + ``` + +`unshift` +: Add the element to the beginning of the array: + + ```js + let fruits = ["Orange", "Pear"]; + + fruits.unshift('Apple'); + + alert( fruits ); // Apple, Orange, Pear + ``` + +Methods `push` and `unshift` can add multiple elements at once: + +```js run +let fruits = ["Apple"]; + +fruits.push("Orange", "Peach"); +fruits.unshift("Pineapple", "Lemon"); + +// ["Pineapple", "Lemon", "Apple", "Orange", "Peach"] +alert( fruits ); +``` + +## Internals + +An array is a special kind of object. The square brackets used to access a property `arr[0]` actually come from the object syntax. Numbers are used as keys. + +They extend objects providing special methods to work with ordered collections of data and also the `length` property. But at the core it's still an object. + +Remember, there are only 7 basic types in JavaScript. Array is an object and thus behaves like an object. + +For instance, it is copied by reference: + +```js run +let fruits = ["Banana"] + +let arr = fruits; // copy by reference (two variables reference the same array) + +alert( arr === fruits ); // true + +arr.push("Pear"); // modify the array by reference + +alert( fruits ); // Banana, Pear - 2 items now +``` + +...But what makes arrays really special is their internal representation. The engine tries to store its elements in the contiguous memory area, one after another, just as depicted on the illustrations in this chapter, and there are other optimizations as well, to make arrays work really fast. + +But they all break if we quit working with an array as with an "ordered collection" and start working with it as if it were a regular object. + +For instance, technically we can do like that: + +```js +let fruits = []; // make an array + +fruits[99999] = 5; // assign a property with the index far greater than its length + +fruits.age = 25; // create a property with an arbitrary name +``` + +That's possible, because arrays are objects at their base. We can add any properties to them. + +But the engine will see that we're working with the array as with a regular object. Array-specific optimizations are not suited for such cases and will be turned off, their benefits disappear. + +The ways to misuse an array: + +- Add a non-numeric property like `arr.test = 5`. +- Make holes, like: add `arr[0]` and then `arr[1000]` (and nothing between them). +- Fill the array in the reverse order, like `arr[1000]`, `arr[999]` and so on. + +Please think of arrays as special structures to work with the *ordered data*. They provide special methods for that. Arrays are carefully tuned inside JavaScript engines to work with contiguous ordered data, please use them this way. And if you need arbitrary keys, chances are high that you actually require a regular object `{}`. + +## Performance + +Methods `push/pop` run fast, while `shift/unshift` are slow. + +![](array-speed.png) + +Why is it faster to work with the end of an array than with its beginning? Let's see what happens during the execution: + +```js +fruits.shift(); // take 1 element from the start +``` + +It's not enough to take and remove the element with the number `0`. Other elements need to be renumbered as well. + +The `shift` operation must do 3 things: + +1. Remove the element with the index `0`. +2. Move all elements to the left, renumber them from the index `1` to `0`, from `2` to `1` and so on. +3. Update the `length` property. + +![](array-shift.png) + +**The more elements in the array, the more time to move them, more in-memory operations.** + +The similar thing happens with `unshift`: to add an element to the beginning of the array, we need first to move existing elements to the right, increasing their indexes. + +And what's with `push/pop`? They do not need to move anything. To extract an element from the end, the `pop` method cleans the index and shortens `length`. + +The actions for the `pop` operation: + +```js +fruits.pop(); // take 1 element from the end +``` + +![](array-pop.png) + +**The `pop` method does not need to move anything, because other elements keep their indexes. That's why it's blazingly fast.** + +The similar thing with the `push` method. + +## Loops + +One of the oldest ways to cycle array items is the `for` loop over indexes: + +```js run +let arr = ["Apple", "Orange", "Pear"]; + +*!* +for (let i = 0; i < arr.length; i++) { +*/!* + alert( arr[i] ); +} +``` + +But for arrays there is another form of loop, `for..of`: + +```js run +let fruits = ["Apple", "Orange", "Plum"]; + +// iterates over array elements +for(let fruit of fruits) { + alert( fruit ); +} +``` + +The `for..of` doesn't give access to the number of the current element, just its value, but in most cases that's enough. And it's shorter. + +Technically, because arrays are objects, it is also possible to use `for..in`: + +```js run +let arr = ["Apple", "Orange", "Pear"]; + +*!* +for (let key in arr) { +*/!* + alert( arr[key] ); // Apple, Orange, Pear +} +``` + +But that's actually a bad idea. There are potential problems with it: + +1. The loop `for..in` iterates over *all properties*, not only the numeric ones. + + There are so-called "array-like" objects in the browser and in other environments, that *look like arrays*. That is, they have `length` and indexes properties, but they may also have other non-numeric properties and methods, which we usually don't need. The `for..in` loop will list them though. So if we need to work with array-like objects, then these "extra" properties can become a problem. + +2. The `for..in` loop is optimized for generic objects, not arrays, and thus is 10-100 times slower. Of course, it's still very fast. The speedup may matter only in bottlenecks or just irrelevant. But still we should be aware of the difference. + +Generally, we shouldn't use `for..in` for arrays. + + +## A word about "length" + +The `length` property automatically updates when we modify the array. To be precise, it is actually not the count of values in the array, but the greatest numeric index plus one. + +For instance, a single element with a large index gives a big length: + +```js run +let fruits = []; +fruits[123] = "Apple"; + +alert( fruits.length ); // 124 +``` + +Note that we usually don't use arrays like that. + +Another interesting thing about the `length` property is that it's writable. + +If we increase it manually, nothing interesting happens. But if we decrease it, the array is truncated. The process is irreversible, here's the example: + +```js run +let arr = [1, 2, 3, 4, 5]; + +arr.length = 2; // truncate to 2 elements +alert( arr ); // [1, 2] + +arr.length = 5; // return length back +alert( arr[3] ); // undefined: the values do not return +``` + +So, the simplest way to clear the array is: `arr.length=0`. + + +## new Array() [#new-array] + +There is one more syntax to create an array: + +```js +let arr = *!*new Array*/!*("Apple", "Pear", "etc"); +``` + +It's rarely used, because square brackets `[]` are shorter. Also there's a tricky feature with it. + +If `new Array` is called with a single argument which is a number, then it creates an array *without items, but with the given length*. + +Let's see how one can shoot himself in the foot: + +```js run +let arr = new Array(2); // will it create an array of [2] ? + +alert( arr[0] ); // undefined! no elements. + +alert( arr.length ); // length 2 +``` + +In the code above, `new Array(number)` has all elements `undefined`. + +To evade such surprises, we usually use square brackets, unless we really know what we're doing. + +## Multidimentional arrays + +Arrays can have items that are also arrays. We can use it for multidimentional arrays, to store matrices: + +```js run +let matrix = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] +]; + +alert( matrix[1][1] ); // the central element +``` + +## toString + +Arrays have their own implementation of `toString` method that returns a comma-separated list of elements. + +For instance: + + +```js run +let arr = [1, 2, 3]; + +alert( arr ); // 1,2,3 +alert( String(arr) === '1,2,3' ); // true +``` + +Also, let's try this: + +```js run +alert( [] + 1 ); // "1" +alert( [1] + 1 ); // "11" +alert( [1,2] + 1 ); // "1,21" +``` + +Arrays do not have `Symbol.toPrimitive`, neither a viable `valueOf`, they implement only `toString` conversion, so here `[]` becomes an empty string, `[1]` becomes `"1"` and `[1,2]` becomes `"1,2". + +When the binary plus `"+"` operator adds something to a string, it converts it to a string as well, so the next step looks like this: + +```js run +alert( "" + 1 ); // "1" +alert( "1" + 1 ); // "11" +alert( "1,2" + 1 ); // "1,21" +``` + +## Summary + +Array is a special kind of objects, suited to store and manage ordered data items. + +- The declaration: + + ```js + // square brackets (usual) + let arr = [item1, item2...]; + + // new Array (exceptionally rare) + let arr = new Array(item1, item2...); + ``` + + The call to `new Array(number)` creates an array with the given length, but without elements. + +- The `length` property is the array length or, to be precise, its last numeric index plus one. It is auto-adjusted by array methods. +- If we shorten `length` manually, the array is truncated. + +We can use an array as a deque with the following operations: + +- `push(...items)` adds `items` to the end. +- `pop()` removes the element from the end and returns it. +- `shift()` removes the element from the beginning and returns it. +- `unshift(...items)` adds items to the beginning. + +To loop over the elements of the array: + - `for(let i=0; i. + diff --git a/1-js/05-data-types/04-array/queue.png b/1-js/05-data-types/04-array/queue.png new file mode 100644 index 00000000..5e1fb640 Binary files /dev/null and b/1-js/05-data-types/04-array/queue.png differ diff --git a/1-js/05-data-types/04-array/queue@2x.png b/1-js/05-data-types/04-array/queue@2x.png new file mode 100644 index 00000000..6acfc83d Binary files /dev/null and b/1-js/05-data-types/04-array/queue@2x.png differ diff --git a/1-js/05-data-types/04-array/stack.png b/1-js/05-data-types/04-array/stack.png new file mode 100644 index 00000000..d1c9cb9a Binary files /dev/null and b/1-js/05-data-types/04-array/stack.png differ diff --git a/1-js/05-data-types/04-array/stack@2x.png b/1-js/05-data-types/04-array/stack@2x.png new file mode 100644 index 00000000..b3835fa4 Binary files /dev/null and b/1-js/05-data-types/04-array/stack@2x.png differ diff --git a/1-js/05-data-types/05-array-methods/1-camelcase/_js.view/solution.js b/1-js/05-data-types/05-array-methods/1-camelcase/_js.view/solution.js new file mode 100644 index 00000000..024d6d6c --- /dev/null +++ b/1-js/05-data-types/05-array-methods/1-camelcase/_js.view/solution.js @@ -0,0 +1,8 @@ +function camelize(str) { + return str + .split('-') // my-long-word -> ['my', 'long', 'word'] + .map( + (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1) + ) // ['my', 'long', 'word'] -> ['my', 'Long', 'Word'] + .join(''); // ['my', 'Long', 'Word'] -> myLongWord +} diff --git a/1-js/05-data-types/05-array-methods/1-camelcase/_js.view/test.js b/1-js/05-data-types/05-array-methods/1-camelcase/_js.view/test.js new file mode 100644 index 00000000..bcf5e955 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/1-camelcase/_js.view/test.js @@ -0,0 +1,19 @@ +describe("camelize", function() { + + it("leaves an empty line as is", function() { + assert.equal(camelize(""), ""); + }); + + it("turns background-color into backgroundColor", function() { + assert.equal(camelize("background-color"), "backgroundColor"); + }); + + it("turns list-style-image into listStyleImage", function() { + assert.equal(camelize("list-style-image"), "listStyleImage"); + }); + + it("turns -webkit-transition into WebkitTransition", function() { + assert.equal(camelize("-webkit-transition"), "WebkitTransition"); + }); + +}); \ No newline at end of file diff --git a/2-ui/1-document/17-coordinates/2-position-at/solution.md b/1-js/05-data-types/05-array-methods/1-camelcase/solution.md similarity index 100% rename from 2-ui/1-document/17-coordinates/2-position-at/solution.md rename to 1-js/05-data-types/05-array-methods/1-camelcase/solution.md diff --git a/1-js/05-data-types/05-array-methods/1-camelcase/task.md b/1-js/05-data-types/05-array-methods/1-camelcase/task.md new file mode 100644 index 00000000..6e27d9ce --- /dev/null +++ b/1-js/05-data-types/05-array-methods/1-camelcase/task.md @@ -0,0 +1,19 @@ +importance: 5 + +--- + +# Translate border-left-width to borderLeftWidth + +Write the function `camelize(str)` that changes dash-separated words like "my-short-string" into camel-cased "myShortString". + +That is: removes all dashes, each word after dash becomes uppercased. + +Examples: + +```js +camelize("background-color") == 'backgroundColor'; +camelize("list-style-image") == 'listStyleImage'; +camelize("-webkit-transition") == 'WebkitTransition'; +``` + + diff --git a/1-js/05-data-types/05-array-methods/11-array-unique/_js.view/solution.js b/1-js/05-data-types/05-array-methods/11-array-unique/_js.view/solution.js new file mode 100644 index 00000000..d15cea2c --- /dev/null +++ b/1-js/05-data-types/05-array-methods/11-array-unique/_js.view/solution.js @@ -0,0 +1,11 @@ +function unique(arr) { + let result = []; + + for (let str of arr) { + if (!result.includes(str)) { + result.push(str); + } + } + + return result; +} diff --git a/1-js/05-data-types/05-array-methods/11-array-unique/_js.view/test.js b/1-js/05-data-types/05-array-methods/11-array-unique/_js.view/test.js new file mode 100644 index 00000000..cfc7b1fc --- /dev/null +++ b/1-js/05-data-types/05-array-methods/11-array-unique/_js.view/test.js @@ -0,0 +1,15 @@ +describe("unique", function() { + it("removes non-unique elements", function() { + let strings = ["Hare", "Krishna", "Hare", "Krishna", + "Krishna", "Krishna", "Hare", "Hare", ":-O" + ]; + + assert.deepEqual(unique(strings), ["Hare", "Krishna", ":-O"]); + }); + + it("does not change the source array", function() { + let strings = ["Krishna", "Krishna", "Hare", "Hare"]; + unique(strings); + assert.deepEqual(strings, ["Krishna", "Krishna", "Hare", "Hare"]); + }); +}); diff --git a/1-js/05-data-types/05-array-methods/11-array-unique/solution.md b/1-js/05-data-types/05-array-methods/11-array-unique/solution.md new file mode 100644 index 00000000..5b565d42 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/11-array-unique/solution.md @@ -0,0 +1,39 @@ +Let's walk the array items: +- For each item we'll check if the resulting array already has that item. +- If it is so, then ignore, otherwise add to results. + +```js run +function unique(arr) { + let result = []; + + for (let str of arr) { + if (!result.includes(str) { + result.push(str); + } + } + + return result; +} + +let strings = ["Hare", "Krishna", "Hare", "Krishna", + "Krishna", "Krishna", "Hare", "Hare", ":-O" +]; + +alert( unique(strings) ); // Hare, Krishna, :-O +``` + +The code works, but there's a potential performance problem in it. + +The method `result.includes(str)` internally walks the array `result` and compares each element against `str` to find the match. + +So if there are `100` elements in `result` and no one matches `str`, then it will walk the whole `result` and do exactly `100` comparisons. And if `result` is large, like `10000`, then there would be `10000` comparisons. + +That's not a problem by itself, because JavaScript engines are very fast, so walk `10000` array is a matter of microseconds. + +But we do such test for each element of `arr`, in the `for` loop. + +So if `arr.length` is `10000` we'll have something like `10000*10000` = 100 millions of comparisons. That's a lot. + +So the solution is only good for small arrays. + +Further in the chapter we'll see how to optimize it. diff --git a/1-js/05-data-types/05-array-methods/11-array-unique/task.md b/1-js/05-data-types/05-array-methods/11-array-unique/task.md new file mode 100644 index 00000000..5b56d362 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/11-array-unique/task.md @@ -0,0 +1,23 @@ +importance: 4 + +--- + +# Filter unique array members + +Let `arr` be an array. + +Create a function `unique(arr)` that should return an array with unique items of `arr`. + +For instance: + +```js +function unique(arr) { + /* your code */ +} + +let strings = ["Hare", "Krishna", "Hare", "Krishna", + "Krishna", "Krishna", "Hare", "Hare", ":-O" +]; + +alert( unique(strings) ); // Hare, Krishna, :-O +``` diff --git a/1-js/05-data-types/05-array-methods/2-filter-range/_js.view/solution.js b/1-js/05-data-types/05-array-methods/2-filter-range/_js.view/solution.js new file mode 100644 index 00000000..0bdfbae5 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/2-filter-range/_js.view/solution.js @@ -0,0 +1,5 @@ + +function filterRange(arr, a, b) { + // added brackets around the expression for better readability + return arr.filter(item => (a <= item && item <= b)); +} \ No newline at end of file diff --git a/1-js/05-data-types/05-array-methods/2-filter-range/_js.view/test.js b/1-js/05-data-types/05-array-methods/2-filter-range/_js.view/test.js new file mode 100644 index 00000000..fb26c8dc --- /dev/null +++ b/1-js/05-data-types/05-array-methods/2-filter-range/_js.view/test.js @@ -0,0 +1,21 @@ +describe("filterRange", function() { + + it("returns the filtered values", function() { + + let arr = [5, 3, 8, 1]; + + let filtered = filterRange(arr, 1, 4); + + assert.deepEqual(filtered, [3, 1]); + }); + + it("doesn't change the array", function() { + + let arr = [5, 3, 8, 1]; + + let filtered = filterRange(arr, 1, 4); + + assert.deepEqual(arr, [5,3,8,1]); + }); + +}); \ No newline at end of file diff --git a/2-ui/1-document/18-coordinates-document/2-position-at-absolute/solution.md b/1-js/05-data-types/05-array-methods/2-filter-range/solution.md similarity index 100% rename from 2-ui/1-document/18-coordinates-document/2-position-at-absolute/solution.md rename to 1-js/05-data-types/05-array-methods/2-filter-range/solution.md diff --git a/1-js/05-data-types/05-array-methods/2-filter-range/task.md b/1-js/05-data-types/05-array-methods/2-filter-range/task.md new file mode 100644 index 00000000..fb6f06ee --- /dev/null +++ b/1-js/05-data-types/05-array-methods/2-filter-range/task.md @@ -0,0 +1,22 @@ +importance: 4 + +--- + +# Filter "in place" + +Write a function `filterRange(arr, a, b)` that gets an array `arr`, looks for elements between `a` and `b` in it and returns an array of them. + +The function should not modify the array. It should return the new array. + +For instance: + +```js +let arr = [5, 3, 8, 1]; + +let filtered = filterRange(arr, 1, 4); + +alert( filtered ); // 3,1 (matching values) + +alert( arr ); // 5,3,8,1 (not modified) +``` + diff --git a/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/solution.js b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/solution.js new file mode 100644 index 00000000..61cda126 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/solution.js @@ -0,0 +1,15 @@ + + +function filterRangeInPlace(arr, a, b) { + + for (let i = 0; i < arr.length; i++) { + let val = arr[i]; + + // remove if outside of the interval + if (val < a || val > b) { + arr.splice(i, 1); + i--; + } + } + +} \ No newline at end of file diff --git a/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/test.js b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/test.js new file mode 100644 index 00000000..db32d9a1 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/test.js @@ -0,0 +1,16 @@ +describe("filterRangeInPlace", function() { + + it("returns the filtered values", function() { + + let arr = [5, 3, 8, 1]; + + filterRangeInPlace(arr, 1, 4); + + assert.deepEqual(arr, [3, 1]); + }); + + it("doesn't return anything", function() { + assert.isUndefined(filterRangeInPlace([1,2,3], 1, 4)); + }); + +}); \ No newline at end of file diff --git a/2-ui/1-document/18-coordinates-document/3-position-at-2/solution.md b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/solution.md similarity index 100% rename from 2-ui/1-document/18-coordinates-document/3-position-at-2/solution.md rename to 1-js/05-data-types/05-array-methods/3-filter-range-in-place/solution.md diff --git a/1-js/05-data-types/05-array-methods/3-filter-range-in-place/task.md b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/task.md new file mode 100644 index 00000000..a737a525 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/task.md @@ -0,0 +1,18 @@ +importance: 4 + +--- + +# Filter range "in place" + +Write a function `filterRangeInPlace(arr, a, b)` that gets an array `arr` and removes from it all values except those that are between `a` and `b. The test is: `a ≤ arr[i] ≤ b`. + +The function should only modify the array. It should not return anything. + +For instance: +```js +let arr = [5, 3, 8, 1]; + +filterRangeInPlace(arr, 1, 4); // removed the numbers except from 1 to 4 + +alert( arr ); // [3, 1] +``` diff --git a/1-js/05-data-types/05-array-methods/4-sort-back/solution.md b/1-js/05-data-types/05-array-methods/4-sort-back/solution.md new file mode 100644 index 00000000..5a9f076b --- /dev/null +++ b/1-js/05-data-types/05-array-methods/4-sort-back/solution.md @@ -0,0 +1,10 @@ + + +```js run +let arr = [5, 2, 1, -10, 8]; + +arr.sort((a,b) => b - a); + +alert( arr ); +``` + diff --git a/1-js/05-data-types/05-array-methods/4-sort-back/task.md b/1-js/05-data-types/05-array-methods/4-sort-back/task.md new file mode 100644 index 00000000..05a08aad --- /dev/null +++ b/1-js/05-data-types/05-array-methods/4-sort-back/task.md @@ -0,0 +1,14 @@ +importance: 4 + +--- + +# Sort in the reverse order + +```js +let arr = [5, 2, 1, -10, 8]; + +// ... your code to sort it in the reverse order + +alert( arr ); // 8, 5, 2, 1, -10 +``` + diff --git a/1-js/05-data-types/05-array-methods/5-copy-sort-array/solution.md b/1-js/05-data-types/05-array-methods/5-copy-sort-array/solution.md new file mode 100644 index 00000000..8537b129 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/5-copy-sort-array/solution.md @@ -0,0 +1,17 @@ +We can use `slice()` to make a copy and run the sort on it: + +```js run +function copySorted(arr) { + return arr.slice().sort(); +} + +let arr = ["HTML", "JavaScript", "CSS"]; + +*!* +let sorted = copySorted(arr); +*/!* + +alert( sorted ); +alert( arr ); +``` + diff --git a/1-js/05-data-types/05-array-methods/5-copy-sort-array/task.md b/1-js/05-data-types/05-array-methods/5-copy-sort-array/task.md new file mode 100644 index 00000000..c1395b4a --- /dev/null +++ b/1-js/05-data-types/05-array-methods/5-copy-sort-array/task.md @@ -0,0 +1,18 @@ +importance: 5 + +--- + +# Copy and sort array + +We have an array of strings `arr`. We'd like to have a sorted copy of it, but keep `arr` unmodified. + +Create a function `copySorted(arr)` that returns such a copy. + +```js +let arr = ["HTML", "JavaScript", "CSS"]; + +let sorted = copySorted(arr); + +alert( sorted ); // CSS, HTML, JavaScript +alert( arr ); // HTML, JavaScript, CSS (no changes) +``` diff --git a/1-js/05-data-types/05-array-methods/6-array-get-names/solution.md b/1-js/05-data-types/05-array-methods/6-array-get-names/solution.md new file mode 100644 index 00000000..f44a2b81 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/6-array-get-names/solution.md @@ -0,0 +1,13 @@ +```js run + +let john = { name: "John", age: 25 }; +let pete = { name: "Pete", age: 30 }; +let mary = { name: "Mary", age: 28 }; + +let users = [ john, pete, mary ]; + +let names = users.map(item => item.name); + +alert( names ); // John, Pete, Mary +``` + diff --git a/1-js/05-data-types/05-array-methods/6-array-get-names/task.md b/1-js/05-data-types/05-array-methods/6-array-get-names/task.md new file mode 100644 index 00000000..74c8a9d7 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/6-array-get-names/task.md @@ -0,0 +1,22 @@ +importance: 5 + +--- + +# Map to names + +You have an array of `user` objects, each one has `user.name`. Write the code that converts it into an array of names. + +For instance: + +```js no-beautify +let john = { name: "John", age: 25 }; +let pete = { name: "Pete", age: 30 }; +let mary = { name: "Mary", age: 28 }; + +let users = [ john, pete, mary ]; + +let names = /* ... your code */ + +alert( names ); // John, Pete, Mary +``` + diff --git a/1-js/05-data-types/05-array-methods/7-map-objects/solution.md b/1-js/05-data-types/05-array-methods/7-map-objects/solution.md new file mode 100644 index 00000000..5e9b815f --- /dev/null +++ b/1-js/05-data-types/05-array-methods/7-map-objects/solution.md @@ -0,0 +1,51 @@ + +```js run no-beautify +let john = { name: "John", surname: "Smith", id: 1 }; +let pete = { name: "Pete", surname: "Hunt", id: 2 }; +let mary = { name: "Mary", surname: "Key", id: 3 }; + +let users = [ john, pete, mary ]; + +*!* +let usersMapped = users.map(user => ({ + fullName: `${user.name} ${user.surname}`, + id: user.id +})); +*/!* + +/* +usersMapped = [ + { fullName: "John Smith", id: 1 }, + { fullName: "Pete Hunt", id: 2 }, + { fullName: "Mary Key", id: 3 } +] +*/ + +alert( usersMapped[0].id ) // 1 +alert( usersMapped[0].fullName ) // John Smith +``` + +Please note that in for the arrow functions we need to use additional brackets. + +We can't write like this: +```js +let usersMapped = users.map(user => *!*{*/!* + fullName: `${user.name} ${user.surname}`, + id: user.id +}); +``` + +As we remember, there are two arrow functions: without body `value => expr` and with body `value => {...}`. + +Here JavaScript would treat `{` as the start of function body, not the start of the object. The workaround is to wrap them in the "normal" brackets: + +```js +let usersMapped = users.map(user => *!*({*/!* + fullName: `${user.name} ${user.surname}`, + id: user.id +}); +``` + +Now fine. + + diff --git a/1-js/05-data-types/05-array-methods/7-map-objects/task.md b/1-js/05-data-types/05-array-methods/7-map-objects/task.md new file mode 100644 index 00000000..b11d1215 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/7-map-objects/task.md @@ -0,0 +1,36 @@ +importance: 5 + +--- + +# Map to objects + +You have an array of `user` objects, each one has `name`, `surname` and `id`. + +Write the code to create another array from it, of objects with `id` and `fullName`, where `fullName` is generated from `name` and `surname`. + +For instance: + +```js no-beautify +let john = { name: "John", surname: "Smith", id: 1 }; +let pete = { name: "Pete", surname: "Hunt", id: 2 }; +let mary = { name: "Mary", surname: "Key", id: 3 }; + +let users = [ john, pete, mary ]; + +*!* +let usersMapped = /* ... your code ... */ +*/!* + +/* +usersMapped = [ + { fullName: "John Smith", id: 1 }, + { fullName: "Pete Hunt", id: 2 }, + { fullName: "Mary Key", id: 3 } +] +*/ + +alert( usersMapped[0].id ) // 1 +alert( usersMapped[0].fullName ) // John Smith +``` + +So, actually you need to map one array of objects to another. Try using `=>` here. There's a small catch. \ No newline at end of file diff --git a/1-js/05-data-types/05-array-methods/8-sort-objects/solution.md b/1-js/05-data-types/05-array-methods/8-sort-objects/solution.md new file mode 100644 index 00000000..2c7c5181 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/8-sort-objects/solution.md @@ -0,0 +1,17 @@ +```js run no-beautify +function sortByName(arr) { + arr.sort((a, b) => a.name > b.name); +} + +let john = { name: "John", age: 25 }; +let pete = { name: "Pete", age: 30 }; +let mary = { name: "Mary", age: 28 }; + +let arr = [ john, pete, mary ]; + +sortByName(arr); + +// now sorted is: [john, mary, pete] +alert(arr[1].name) // Mary +``` + diff --git a/1-js/05-data-types/05-array-methods/8-sort-objects/task.md b/1-js/05-data-types/05-array-methods/8-sort-objects/task.md new file mode 100644 index 00000000..935c27fc --- /dev/null +++ b/1-js/05-data-types/05-array-methods/8-sort-objects/task.md @@ -0,0 +1,23 @@ +importance: 5 + +--- + +# Sort objects + +Write the function `sortByName(users)` that gets an array of objects with property `name` and sorts it. + +For instance: + +```js no-beautify +let john = { name: "John", age: 25 } +let pete = { name: "Pete", age: 30 } +let mary = { name: "Mary", age: 28 } + +let arr = [ john, pete, mary ]; + +lsortByName(arr); + +// now: [john, mary, pete] +alert(arr[1].name) // Mary +``` + diff --git a/1-js/05-data-types/05-array-methods/9-average-age/solution.md b/1-js/05-data-types/05-array-methods/9-average-age/solution.md new file mode 100644 index 00000000..87bb3a94 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/9-average-age/solution.md @@ -0,0 +1,14 @@ +```js run +function getAverageAge(users) { + return arr.reduce((prev, user) => prev + user.age, 0) / arr.length; +} + +let john = { name: "John", age: 25 } +let pete = { name: "Pete", age: 30 } +let mary = { name: "Mary", age: 29 } + +let arr = [ john, pete, mary ]; + +alert( getAverageAge(arr) ); // 28 +``` + diff --git a/1-js/05-data-types/05-array-methods/9-average-age/task.md b/1-js/05-data-types/05-array-methods/9-average-age/task.md new file mode 100644 index 00000000..ccb2142d --- /dev/null +++ b/1-js/05-data-types/05-array-methods/9-average-age/task.md @@ -0,0 +1,22 @@ +importance: 4 + +--- + +# Get average age + +Write the function `getAverageAge(users)` that gets an array of objects with property `age` and gets the average. + +The formula for the average is `(age1 + age2 + ... + ageN) / N`. + +For instance: + +```js no-beautify +let john = { name: "John", age: 25 } +let pete = { name: "Pete", age: 30 } +let mary = { name: "Mary", age: 29 } + +let arr = [ john, pete, mary ]; + +alert( getAverageAge(arr) ); // (25+30+29)/3 = 28 +``` + diff --git a/1-js/05-data-types/05-array-methods/article.md b/1-js/05-data-types/05-array-methods/article.md new file mode 100644 index 00000000..a16707fc --- /dev/null +++ b/1-js/05-data-types/05-array-methods/article.md @@ -0,0 +1,705 @@ +# Array methods + +Arrays provide a lot of methods. To make things easier, in this chapter they are split into groups. + +[cut] + +## Add/remove items + +We already know methods that add and remove items from the beginning or the end: + +- `arr.push(...items)` -- adds items to the end, +- `arr.pop()` -- extracts an item from the end, +- `arr.shift()` -- extracts an item from the beginning, +- `arr.unshift(...items)` -- adds items to the beginning. + +Here are few others. + +### splice + +How to delete an element from the array? + +The arrays are objects, so we can try to use `delete`: + +```js run +let arr = ["I", "go", "home"]; + +delete arr[1]; // remove "go" + +alert( arr[1] ); // undefined + +// now arr = ["I", , "home"]; +alert( arr.length ); // 3 +``` + +The element was removed, but the array still has 3 elements, we can see that `arr.length == 3`. + +That's natural, because `delete obj.key` removes a value by the `key`. It's all it does. Fine for objects. But for arrays we usually want the rest of elements to shift and occupy the freed place. We expect to have a shorter array now. + +So, special methods should be used. + +The [arr.splice(str)](mdn:js/Array/splice) method is a swiss army knife for arrays. It can do everything: add, remove and insert elements. + +The syntax is: + +```js +arr.splice(index[, deleteCount, elem1, ..., elemN]) +``` + +It starts from the position `index`: removes `deleteCount` elements and then inserts `elem1, ..., elemN` at their place. Returns the array of removed elements. + +This method is easy to grasp by examples. + +Let's start with the deletion: + +```js run +let arr = ["I", "study", "JavaScript"]; + +*!* +arr.splice(1, 1); // from index 1 remove 1 element +*/!* + +alert( arr ); // ["I", "JavaScript"] +``` + +Easy, right? Starting from the index `1` it removed `1` element. + +In the next example we remove 3 elements and replace them by the other two: + +```js run +let arr = [*!*"I", "study", "JavaScript",*/!* "right", "now"]; + +// remove 3 first elements and replace them by another +arr.splice(0, 3, "Let's", "dance") + +alert( arr ) // now [*!*"Let's", "dance"*/!*, "right", "now"] +``` + +Here we can see that `splice` returns the array of removed elements: + +```js run +let arr = [*!*"I", "study",*/!* "JavaScript", "right", "now"]; + +// remove 2 first elements +let removed = arr.splice(0, 2); + +alert( removed ); // "I", "study" <-- array of removed elements +``` + +The `splice` method is also able to insert the elements without any removals. For that we need to set `deleteCount` to `0`: + +```js run +let arr = ["I", "study", "JavaScript"]; + +// from index 2 +// delete 0 +// then insert "complex" and "language" +arr.splice(2, 0, "complex", "language"); + +alert( arr ); // "I", "study", "complex", "language", "JavaScript" +``` + +````smart header="Negative indexes allowed" +Here and in other array methods, negative indexes are allowed. The specify the position from the end of the array, like here: + +```js run +let arr = [1, 2, 5] + +// from index -1 (one step from the end) +// delete 0 elements, +// then insert 3 and 4 +arr.splice(-1, 0, 3, 4); + +alert( arr ); // 1,2,3,4,5 +``` +```` + +### slice + +The method [arr.slice](mdn:js/Array/slice) is much simpler than similar-looking `arr.splice`. + +The syntax is: + +```js +arr.slice(start, end) +``` + +It returns a new array where it copies all items start index `"start"` to `"end"` (not including `"end"`). Both `start` and `end` can be negative, in that case position from array end is assumed. + +It works like `str.slice`, but makes subarrays instead of substrings. + +For instance: + +```js run +let str = "test"; +let arr = ["t", "e", "s", "t"]; + +alert( str.slice(1, 3) ); // es +alert( arr.slice(1, 3) ); // e,s + +alert( str.slice(-2) ); // st +alert( arr.slice(-2) ); // s,t +``` + +### concat + +The method [arr.concat](mdn:js/Array/concat) joins the array with other arrays and/or items. + +The syntax is: + +```js +arr.concat(arg1, arg2...) +``` + +It accepts any number of arguments -- either arrays or values. + +The result is a new array containing items from `arr`, then `arg1`, `arg2` etc. + +If an argument is an array or has `Symbol.isConcatSpreadable` property, then all its elements are copied. Otherwise the argument itself is copied. + +For instance: + +```js run +let arr = [1, 2]; + +// merge arr with [3,4] +alert( arr.concat([3, 4])); // 1,2,3,4 + +// merge arr with [3,4] and [5,6] +alert( arr.concat([3, 4], [5, 6])); // 1,2,3,4,5,6 + +// merge arr with [3,4], then add values 5 and 6 +alert( arr.concat([3, 4], 5, 6)); // 1,2,3,4,5,6 +``` + +Normally, it only copies elements from arrays ("spreads" them), other objects even if they look like arrays and added as a whole: + +```js run +let arr = [1, 2]; + +let arrayLike = { + 0: "something", + length: 1 +}; + +alert( arr.concat(arrayLike) ); // 1,2,[object Object] +//[1, 2, arrayLike] +``` + +...But if an array-like object has `Symbol.isConcatSpreadable` property, then its elements are added instead: + +```js run +let arr = [1, 2]; + +let arrayLike = { + 0: "something", + 1: "else", +*!* + [Symbol.isConcatSpreadable]: true, +*/!* + length: 2 +}; + +alert( arr.concat(arrayLike) ); // 1,2,something,else +``` + +## Searching in array + +These are methods to search for something in an array. + +### indexOf/lastIndexOf and includes + +The methods [arr.indexOf](mdn:js/Array/indexOf), [arr.lastIndexOf](mdn:js/Array/lastIndexOf) and [arr.includes](mdn:js/Array/includes) have the same syntax and do essentially the same as their string counterparts, but operate on items instead of characters: + +- `arr.indexOf(item, from)` looks for `item` starting from index `from`, and returns the index where it was found, otheriwse `-1`. +- `arr.lastIndexOf(item, from)` -- same, but looks from right to left. +- `arr.includes(item, from)` -- looks for `item` starting from index `from`, returns `true` if found. + +For instance: + +```js run +let arr = [1, 0, false]; + +alert( arr.indexOf(0) ); // 1 +alert( arr.indexOf(false) ); // 2 +alert( arr.indexOf(null) ); // -1 + +alert( arr.includes(1) ); // true +``` + +Note that the methods use `===` comparison. So, if we look for `false`, it finds exactly `false` and not the zero. + +If we wan to check for inclusion, and don't want to know the exact index, then `arr.includes` is preferred. + + +### find and findIndex + +Imagine we have an array of objects. How do we find an object with the specific condition? + +Here the [arr.find](mdn:js/Array/find) method comes in handy. + +The syntax is: +```js +let result = arr.find(function(item, index, array) { + // should return true if the item is what we are looking for +}); +``` + +The function is called repetitively for each element of the array: + +- `item` is the element. +- `index` is its index. +- `array` is the array itself. + +If it returns `true`, the search is stopped, the `item` is returned. If nothing found, `undefined` is returned. + +For example, we have an array of users, each with the fields `id` and `name`. Let's find the one with `id == 1`: + +```js run +let users = [ + {id: 1, name: "John"}, + {id: 2, name: "Pete"}, + {id: 3, name: "Mary"} +]; + +let user = users.find(item => item.id == 1); + +alert(user.name); // John +``` + +In real life arrays of objects is a common thing, so the `find` method is very useful. + +Note that in the example we provide to `find` a single-argument function `item => item.id == 1`. Other parameters of `find` are rarely used. + +The [arr.findIndex](mdn:js/Array/findIndex) method is essentially the same, but it returns the index where the element was found instead of the element itself. + +### filter + +The `find` method looks for a single (first) element that makes the function return `true`. + +If there may be many, we can use [arr.filter(fn)](mdn:js/Array/filter). + +The syntax is roughly the same as `find`, but it returns an array of matching elements: + +```js +let results = arr.filter(function(item, index, array) { + // should return true if the item passes the filter +}); +``` + +For instance: + +```js run +let users = [ + {id: 1, name: "John"}, + {id: 2, name: "Pete"}, + {id: 3, name: "Mary"} +]; + +// returns array of the first two users +let someUsers = users.filter(item => item.id < 3); + +alert(someUsers.length); // 2 +``` + +## Transform an array + +This section is about the methods transforming or reordering the array. + + +### map + +The [arr.map](mdn:js/Array/map) method is one of the most useful and often used. + +The syntax is: + +```js +let result = arr.map(function(item, index, array) { + // returns the new value instead of item +} +``` + +It calls the function for each element of the array and returns the array of results. + +For instance, here we transform each element into its length: + +```js run +let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length) +alert(lengths); // 5,7,6 +``` + +### sort(fn) + +The method [arr.sort](mdn:js/Array/sort) sorts the array *in place*. + +For instance: + +```js run +let arr = [ 1, 2, 15 ]; + +// the method reorders the content of arr (and returns it) +arr.sort(); + +alert( arr ); // *!*1, 15, 2*/!* +``` + +Did you notice anything strange in the outcome? + +The order became `1, 15, 2`. Incorrect. But why? + +**The items are sorted as strings by default.** + +Literally, all elements are converted to strings and then compared. So, the lexicographic ordering is applied and indeed `"2" > "15"`. + +To use our own sorting order, we need to supply a function of two arguments as the argument of `arr.sort()`. + +The function should work like this: +```js +function compare(a, b) { + if (a > b) return 1; + if (a == b) return 0; + if (a < b) return -1; +} +``` + +For instance: + +```js run +function compareNumeric(a, b) { + if (a > b) return 1; + if (a == b) return 0; + if (a < b) return -1; +} + +let arr = [ 1, 2, 15 ]; + +*!* +arr.sort(compareNumeric); +*/!* + +alert(arr); // *!*1, 2, 15*/!* +``` + +Now it works as intended. + +Let's step aside and thing what's happening. The `arr` can be array of anything, right? It may contain numbers or strings or html elements or whatever. We have a set of *something*. To sort it, we need an *ordering function* that knows how to compare its elements. The default is a string order. + +The `arr.sort(fn)` method has a built-in implementation of sorting algorithm. We don't need to care how it exactly works (an optimized [quicksort](https://en.wikipedia.org/wiki/Quicksort) most of the time). It will walk the array, compare its elements using the provided function and reorder them, all we need is to provide the `fn` which does the comparison. + +By the way, if we ever want to know which elements are compared -- nothing prevents from alerting them: + +```js run +[1, -2, 15, 2, 0, 8].sort(function(a, b) { + alert( a + " <> " + b ); +}); +``` + +The algorithm may compare an element multiple times 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". + +That allows to write shorter functions: + +```js run +let arr = [ 1, 2, 15 ]; + +arr.sort(function(a, b) { return a - b; }); + +alert(arr); // *!*1, 2, 15*/!* +``` +```` + +````smart header="Arrow functions for the best" +Remember [arrow functions](info:function-expression#arrow-functions)? We can use them here for neater sorting: + +```js +arr.sort( (a, b) => a - b ); +``` + +This works exactly the same as the other, longer, version above. +```` + +### reverse + +The method [arr.reverse](mdn:js/Array/reverse) reverses the order of elements in `arr`. + +For instance: + +```js run +let arr = [1, 2, 3, 4, 5]; +arr.reverse(); + +alert( arr ); // 5,4,3,2,1 +``` + +It also returns the array `arr` after the reversal. + +### split and join + +Here's the situation from the real life. We are writing a messaging app, and the person enters the comma-delimited list of receivers: `John, Pete, Mary`. But for us an array of names would be much more comfortable than a single string. How to get it? + +The [str.split(delim)](mdn:js/String/split) method does exactly that. It splits the string into an array by the given delimiter `delim`. + +In the example below, we split by a comma followed by space: + +```js run +let names = 'Bilbo, Gandalf, Nazgul'; + +let arr = names.split(', '); + +for (let name of arr) { + alert( `A message to ${name}.` ); // A message to Bilbo (and other names) +} +``` + +The `split` method has an optional second numeric argument -- a limit on the array length. If it is provided, then the extra elements are ignored. In practice it is rarely used though: + +```js run +let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2); + +alert(arr); // Bilbo, Gandalf +``` + +````smart header="Split into letters" +The call to `split(s)` with an empty `s` would split the string into an array of letters: + +```js run +let str = "test"; + +alert( str.split('') ); // t,e,s,t +``` +```` + +The call [arr.join(str)](mdn:js/Array/join) does the reverse to `split`. It creates a string of `arr` items glued by `str` beween them. + +For instance: + +```js run +let arr = ['Bilbo', 'Gandalf', 'Nazgul']; + +let str = arr.join(';'); + +alert( str ); // Bilbo;Gandalf;Nazgul +``` + +### reduce/reduceRight + +When we need to iterate over an array -- we can use `forEach`. + +When we need to iterate and return the data for each element -- we can use `map`. + +The methods [arr.reduce](mdn:js/Array/reduce) and [arr.reduceRight](mdn:js/Array/reduceRight) also belong to that breed, but are a little bit more intricate. They are used to calculate a single value based on the array. + +The syntax is: + +```js +let value = arr.reduce(function(previousValue, item, index, arr) { + // ... +}, initial); +``` + +The function is applied to the elements. You may notice the familiar arguments, starting from the 2nd: + +- `item` -- is the current array item. +- `index` -- is its position. +- `arr` -- is the array. + +So far, like `forEach/map`. But there's one more argument: + +- `previousValue` -- is the result of the previous function call, `initial` for the first call. + +The easiest way to grasp that is by example. + +Here we get a sum of array in one line: + +```js run +let arr = [1, 2, 3, 4, 5] + +let result = arr.reduce((sum, current) => sum + current), 0); + +alert(result); // 15 +``` + +Here we used the most common variant of `reduce` which uses only 2 arguments. + +Let's see the details of what's going on. + +1. On the first run, `sum` is the initial value (the last argument of `reduce`), equals `0`, and `current` is the first array element, equals `1`. So the result is `1`. +2. On the second run, `sum = 1`, we add the second array element (`2`) to it and return. +3. On the 3rd run, `sum = 3` and we add one more element ot it, and so on... + +The calculation flow: + +![](reduce.png) + +Or in the form of a table, where each row represents is a function call on the next array element: + +| |`sum`|`current`|`result`| +|---|-----|---------|---------| +|the first call|`0`|`1`|`1`| +|the second call|`1`|`2`|`3`| +|the third call|`3`|`3`|`6`| +|the fourth call|`6`|`4`|`10`| +|the fifth call|`10`|`5`|`15`| + + +As we can see, the result of the previous call becomes the first argument of the next one. + +We also can omit the initial value: + +```js run +let arr = [1, 2, 3, 4, 5]; + +// removed initial value from reduce (no 0) +let result = arr.reduce((sum, current) => sum + current); + +alert( result ); // 15 +``` + +The result is the same. That's because if there's no initial, then `reduce` takes the first element of the array as the initial value and starts the iteration from the 2nd element. + +The calculation table is the same as above, minus the first row. + +But such use requires an extreme care. If the array is empty, then `reduce` call without initial value gives an error. + +Here's an example: + +```js run +let arr = []; + +// Error: Reduce of empty array with no initial value +// if the initial value existed, reduce would return it for the empty arr. +arr.reduce((sum, current) => sum + current); +``` + + +So it's advised to always specify the initial value. + +The method [arr.reduceRight](mdn:js/Array/reduceRight) does the same, but goes from right to left. + + +## Iterate: forEach + +The [arr.forEach](mdn:js/Array/forEach) method allows to run a function for every element of the array. + +The syntax: +```js +arr.forEach(function(item, index, array) { + // ... do something with item +}); +``` + +For instance, this shows each element of the array: + +```js run +// for each element call alert +["Bilbo", "Gandalf", "Nazgul"].forEach(alert); +``` + +And this code is more elaborate about their positions in the target array: + +```js run +["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => { + alert(`${item} is at index ${index} in ${array}`); +}); +``` + +The result of the function (if it returns any) is thrown away and ignored. + +## Array.isArray + +Arrays do not form a separate language type. They are based on objects. + +So `typeof` does not help to distinguish a plain object from an array: + +```js run +alert(typeof {}); // object +alert(typeof []); // same +``` + +...But arrays are used so often that there's a special method for that: [Array.isArray(value)](mdn:js/Array/isArray). It returns `true` if the `value` is an array, and `false` otherwise. + +```js run +alert(Array.isArray({})); // false + +alert(Array.isArray([])); // true +``` + +## Most methods support "thisArg" + +Almost all array methods that call functions -- like `find`, `filter`, `map`, with a notable exception of `sort`, accept an optional additional parameter `thisArg`. + +That parameter is not explained in the sections above, because it's rarely used. But for completeness we have to cover it. + +Here's the full syntax of these methods: + +```js +arr.find(func, thisArg); +arr.filter(func, thisArg); +arr.map(func, thisArg); +// ... +// thisArg is the optional last argument +``` + +The value of `thisArg` parameter becomes `this` for `func`. + +For instance, here we use an object method as a filter and `thisArg` comes in handy: + +```js run +let user = { + age: 18, + younger(otherUser) { + return otherUser.age < this.age; + } +}; + +let users = [ + {age: 12}, + {age: 16}, + {age: 32} +]; + +*!* +// find all users younger than user +let youngerUsers = users.filter(user.younger, user); +*/!* + +alert(youngerUsers.length); // 2 +``` + +In the call above, we use `user.younger` as a filter and also provide `user` as the context for it. If we didn't provide the context, `users.filter(user.younger)` would call `user.younger` as a standalone function, with `this=undefined`. That would mean an instant error. + +## Other methods + +We covered the most useful methods. But there are few others: + +- [arr.some(fn)](mdn:js/Array/some)/[arr.every(fn)](mdn:js/Array/every) checks the array. + + The function `fn` is called on each element of the array similar to `map`. If any/all results are `true`, returns `true`, otherwise `false`. + +- [arr.fill(value, start, end)](mdn:js/Array/fill) -- fills the array with repeating `value` from index `start` to `end`. + +- [arr.copyWithin(target, start, end)](mdn:js/Array/copyWithin) -- copies its elements from position `start` till position `end` into *itself*, at position `target` (overwrites existing). + +These and other methods are also listed in the [manual](mdn:js/Array). + + +## Summary + +Most used array methods: + +- `split/join` -- convert a string to array and back. +- `splice` -- delete and insert elements at the given position. +- `sort` -- sorts the array. +- `indexOf/lastIndexOf`, `includes` -- look for the value. +- `find/filter` -- return first/all values satisfying the given condition, `findIndex` is like `find`, but returns the index instead of a value. +- `forEach` -- runs a function for each element. +- `map` -- transforms the array through the function. +- `reduce/reduceRight` -- calculates a single value based on array. +- `slice` -- copy the part of the array. + +These methods are used in 95% of cases. For the full list, see the [manual](mdn:js/Array). + +The methods are easy to remember when you use them. Please refer to the tasks for some practice. diff --git a/1-js/05-data-types/05-array-methods/reduce.png b/1-js/05-data-types/05-array-methods/reduce.png new file mode 100644 index 00000000..41476d2a Binary files /dev/null and b/1-js/05-data-types/05-array-methods/reduce.png differ diff --git a/1-js/05-data-types/05-array-methods/reduce@2x.png b/1-js/05-data-types/05-array-methods/reduce@2x.png new file mode 100644 index 00000000..f31647d1 Binary files /dev/null and b/1-js/05-data-types/05-array-methods/reduce@2x.png differ diff --git a/1-js/05-data-types/06-iterable/article.md b/1-js/05-data-types/06-iterable/article.md new file mode 100644 index 00000000..a5a02433 --- /dev/null +++ b/1-js/05-data-types/06-iterable/article.md @@ -0,0 +1,301 @@ + +# Iterables + +*Iterable* objects is a generalization of arrays. That's a concept that allows to make any object useable in a `for..of` loop. + +Arrays by themselves are iterable. But not only arrays. Strings are iterable too, and many other built-in objects as well. + +Iterables are widely used by the core JavaScript, as we'll see many operators and built-in methods rely on them. + +[cut] + +## Symbol.iterator + +We can easily grasp the concept of iterables by making one of our own. + +For instance, we have an object, that is not an array, but looks suitable for `for..of`. + +Like a `range` object that represents an interval of numbers: + +```js +let range = { + from: 1, + to: 5 +}; + +// We want for..of to work: +// for(let num of range) ... num=1,2,3,4,5 +``` + +To make the `range` iterable (and thus let `for..of` work) we need to add a method to the object named `Symbol.iterator` (a special built-in symbol just for that). + +- When `for..of` starts, it calls that method (or errors if none found). +- The method must return an *iterator* -- an object with the method `next`. +- When `for..of` wants the next value, it calls `next()` on that object. +- The result of `next()` must have the form `{done: Boolean, value: any}`, where `done=true` means that the iteration is finished, otherwise `value` must be the new value. + +Here's the full implementation for `range`: + +```js run +let range = { + from: 1, + to: 5 +}; + +// 1. call to for..of initially calls this +range[Symbol.iterator] = function() { + + // 2. ...it returns the iterator: + return { + current: this.from, // start at "range.from", + last: this.to, // end at "range.to" + + // 3. next() is called on each iteration by for..of + next() { + if (this.current <= this.last) { + // 4. it should return the value as an object {done:.., value :...} + return { done: false, value: this.current++ }; + } else { + return { done: true }; + } + } + }; +}; + +for (let num of range) { + alert(num); // 1, then 2, 3, 4, 5 +} +``` + +There is an important separation of concerns in this code: + +- The `range` itself does not have the `next()` method. +- Instead, another object, a so-called "iterator" is created by the call to `range[Symbol.iterator]()`, and it handles the iteration. + +So, the iterator is separate from the object. + +Technically, we may merge them and use `range` itself as the iterator, to make the code simpler. + +Like this: + +```js run +let range = { + from: 1, + to: 5, + + [Symbol.iterator]() { + this.current = this.from; + return this; + }, + + next() { + if (this.current <= this.to) { + return { done: false, value: this.current++ }; + } else { + return { done: true }; + } + } +}; + +for (let num of range) { + alert(num); // 1, then 2, 3, 4, 5 +} +``` + +Now `range[Symbol.iterator]()` returns the `range` object itself, and it has the necessary `next()` method. Sometimes that's fine too. The downside is that now it's impossible to have two `for..of` loops running over the object simultaneously: they'll share the iteration state, because there's only one iterator -- the object itself. + +```smart header="Infinite iterators" +Infinite iterators are also doable. For instance, the `range` becomes infinite for `range.to = Infinity`. Or we can make an iterable object that generates an infinite sequence of pseudorandom numbers. Also can be useful. + +There are no limitations on `next`, it can return more and more values, that's normal. + +Of course, the `for..of` loop over such an iterable would be endless. But we can always stop it using `break`. +``` + + +## String is iterable + +Arrays and strings are most widely used built-in iterables. + +For a string, `for..of` loops over its characters: + +```js run +for(let char of "test") { + alert( char ); // t, then e, then s, then t +} +``` + +And it works right with surrogate pairs! + +```js run +let str = '𝒳😂'; +for(let char of str) { + alert(char); // 𝒳, and then 😂 +} +``` + +## Calling an iterator explicitly + +Normally, internals of iterables are hidden from the external code. There's a `for..of` loop, that works, that's all it needs to know. + +But to understand things a little bit more deeper let's see how to create an iterator explicitly. We'll do that the same way as `for..of`, but with direct calls. + +For instance, this code gets a string iterator and calls it "manually": + +```js run +let str = "Hello"; + +// does the same as +// for (let char of str) alert(char); + +let iterator = str[Symbol.iterator](); + +while(true) { + let result = iterator.next(); + if (result.done) break; + alert(result.value); // outputs characters one by one +} +``` + +That is rarely needed, but gives us more control than `for..of`. For instance, we can split the iteration process: iterate a bit, then stop, do something else, and then resume later. + +## Iterables and array-likes [#array-like] + +There are two official terms that look similar, but are very different. Please be careful to avoid the confusion. + +- *Iterables* are objects that implement the `Symbol.iterator` method, as described above. +- *Array-likes* are objects that have indexes and `length`, so they look like arrays. + +Naturally, they can combine. For instance, strings are both iterable and array-like. + +But an iterable may be not array-like and vice versa. + +For example, the `range` in the example above is iterable, but not array-like, because it does not have indexed properties and `length`. + +And here's the object that is array-like, but not iterable: + +```js run +let arrayLike = { // has indexes and length => array-like + 0: "Hello", + 1: "World", + length: 2 +}; + +*!* +// Error (no Symbol.iterator) +for(let item of arrayLike) {} +*/!* +``` + +What they share in common -- both iterables and array-likes are usually *not arrays*, they don't have `push`, `pop` etc. That's rather inconvenient if we have such an object and want to work with it as with an array. + +## Array.from + +There's a universal method [Array.from](mdn:js/Array/from) that brings them together. It takes an iterable or array-like value and makes a "real" `Array` from it. Then we can call array methods on it. + +For instance: + +```js run +let arrayLike = { + 0: "Hello", + 1: "World", + length: 2 +}; + +*!* +let arr = Array.from(arrayLike); // (*) +*/!* +alert(arr.pop()); // World (method works) +``` + +`Array.from` at the line `(*)` takes the object, examines it for being an iterable or array-like, then makes a new array and copies there all items. + +The same happens for an iterable: + +```js +// assuming that range is taken from the example above +let arr = Array.from(range); +alert(arr); // 1,2,3,4,5 (array toString conversion works) +``` + +The full syntax for `Array.from` allows to provide an optional "mapping" function: +```js +Array.from(obj[, mapFn, thisArg]) +``` + +The second argument `mapFn` should be the function to apply to each element before adding to the array, and `thisArg` allows to set `this` for it. + +For instance: + +```js +// assuming that range is taken from the example above + +// square each number +let arr = Array.from(range, num => num * num); + +alert(arr); // 1,4,9,16,25 +``` + +We can also use `Array.from` to turn a string into an array of characters: + +```js run +let str = '𝒳😂'; + +// splits str into array of characters +let chars = Array.from(str); + +alert(chars[0]); // 𝒳 +alert(chars[1]); // 😂 +alert(chars.length); // 2 +``` + +Unlike `str.split`, it relies on the iterable nature of the string and so, just like `for..of`, correctly works with surrogate pairs. + +Technically here it does the same as: + +```js run +let str = '𝒳😂'; + +let chars = []; // Array.from internally does the same loop +for(let char of str) { + chars.push(char); +} + +alert(chars); +``` + +...But is shorter. + +We can even build surrogate-aware `slice` on it: + +```js run +function slice(str, start, end) { + return Array.from(str).slice(start, end).join(''); +} + +let str = '𝒳😂𩷶'; + +alert( slice(str, 1, 3) ); // 😂𩷶 + +// native method does not support surrogate pairs +alert( str.slice(1, 3) ); // garbage (two pieces from different surrogate pairs) +``` + + +## Summary + +Objects that can be used in `for..of` are called *iterable*. + +- Technically, iterables must implement the method named `Symbol.iterator`. + - The result of `obj[Symbol.iterator]` is called an *iterator*. It handles the further iteration process. + - An iterator must have the method named `next()` that returns an object `{done: Boolean, value: any}`, here `done:true` denotes the iteration end, otherwise the `value` is the next value. +- The `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. + + +Objects that have indexed properties and `length` are called *array-like*. Such objects may also have other properties and methods, but lack built-in methods of arrays. + +If we look inside the specification -- we'll see that most built-in methods assume that they work with iterables or array-likes instead of "real" arrays, because that's more abstract. + +`Array.from(obj[, mapFn, thisArg])` makes a real `Array` of an iterable or array-like `obj`, and then we can use array methods on it. The optional arguments `mapFn` and `thisArg` allow to apply a function to each item. diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/solution.js b/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/solution.js new file mode 100644 index 00000000..de504e1e --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/solution.js @@ -0,0 +1,3 @@ +function unique(arr) { + return Array.from(new Set(arr)); +} diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/test.js b/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/test.js new file mode 100644 index 00000000..cfc7b1fc --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/test.js @@ -0,0 +1,15 @@ +describe("unique", function() { + it("removes non-unique elements", function() { + let strings = ["Hare", "Krishna", "Hare", "Krishna", + "Krishna", "Krishna", "Hare", "Hare", ":-O" + ]; + + assert.deepEqual(unique(strings), ["Hare", "Krishna", ":-O"]); + }); + + it("does not change the source array", function() { + let strings = ["Krishna", "Krishna", "Hare", "Hare"]; + unique(strings); + assert.deepEqual(strings, ["Krishna", "Krishna", "Hare", "Hare"]); + }); +}); diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/solution.md b/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/solution.md similarity index 100% rename from 2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/solution.md rename to 1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/solution.md diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/task.md b/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/task.md new file mode 100644 index 00000000..d6803003 --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/task.md @@ -0,0 +1,27 @@ +importance: 5 + +--- + +# Filter unique array members + +Let `arr` be an array. + +Create a function `unique(arr)` that should return an array with unique items of `arr`. + +For instance: + +```js +function unique(arr) { + /* your code */ +} + +let values = ["Hare", "Krishna", "Hare", "Krishna", + "Krishna", "Krishna", "Hare", "Hare", ":-O" +]; + +alert( unique(values) ); // Hare, Krishna, :-O +``` + +P.S. Here strings are used, but can be values of any type. + +P.P.S. Use `Set` to store unique values. diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/solution.js b/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/solution.js new file mode 100644 index 00000000..b9f5016f --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/solution.js @@ -0,0 +1,11 @@ + +function aclean(arr) { + let map = new Map(); + + for(let word of arr) { + let sorted = word.toLowerCase().split("").sort().join(""); + map.set(sorted, word); + } + + return Array.from(map.values()); +} \ No newline at end of file diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/test.js b/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/test.js new file mode 100644 index 00000000..75acb36b --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/test.js @@ -0,0 +1,24 @@ +function intersection(arr1, arr2) { + return arr1.filter(item => arr2.includes(item)); +} + +describe("aclean", function() { + + it("returns exactly 1 word from each anagram set", function() { + let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; + + let result = aclean(arr); + assert.equal(result.length, 3); + + assert.equal(intersection(result, ["nap", "PAN"]).length, 1); + assert.equal(intersection(result, ["teachers", "cheaters", "hectares"]).length, 1); + assert.equal(intersection(result, ["ear", "era"]).length, 1); + + }); + + it("is case-insensitive", function() { + let arr = ["era", "EAR"]; + assert.equal(aclean(arr).length, 1); + }); + +}); \ No newline at end of file diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/solution.md b/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/solution.md new file mode 100644 index 00000000..f95b41aa --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/solution.md @@ -0,0 +1,77 @@ +To find all anagrams, let's split every word to letters and sort them. When letter-sorted, all anagrams are same. + +For instance: + +``` +nap, pan -> anp +ear, era, are -> aer +cheaters, hectares, teachers -> aceehrst +... +``` + +We'll use the letter-sorted variants as map keys to store only one value per each key: + +```js run +function aclean(arr) { + let map = new Map(); + + for(let word of arr) { + // split the word by letters, sort them and join back +*!* + let sorted = word.toLowerCase().split('').sort().join(''); // (*) +*/!* + map.set(sorted, word); + } + + return Array.from(map.values()); +} + +let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; + +alert( aclean(arr) ); +``` + +Letter-sorting is done by the chain of calls in the line `(*)`. + +For convenience let's split it into multiple lines: + +```js +let sorted = arr[i] // PAN + .toLowerCase() // pan + .split('') // ['p','a','n'] + .sort() // ['a','n','p'] + .join(''); // anp +``` + +Two different words `'PAN'` and `'nap'` receive the same letter-sorted form `'anp'`. + +The next line put the word into the map: + +```js +map.set(sorted, word); +``` + +If we ever meet a word the same letter-sorted form again, then it would overwrite the previous value with the same key in the map. So we'll always have at maximum one word per letter-form. + +At the end `Array.from(map.values())` takes an iterable over map values (we don't need keys in the result) and returns an array of them. + +Here we could also use a plain object instead of the `Map`, because keys are strings. + +That's how the solution can look: + +```js run +function aclean(arr) { + let obj = {}; + + for (let i = 0; i < arr.length; i++) { + let sorted = arr[i].toLowerCase().split("").sort().join(""); + obj[sorted] = arr[i]; + } + + return Array.from(Object.values(obj)); +} + +let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; + +alert( aclean(arr) ); +``` diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/task.md b/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/task.md new file mode 100644 index 00000000..731fd2c2 --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/task.md @@ -0,0 +1,28 @@ +importance: 4 + +--- + +# Filter anagrams + +[Anagrams](https://en.wikipedia.org/wiki/Anagram) are words that have the same number of same letters, but in different order. + +For instance: + +``` +nap - pan +ear - are - era +cheaters - hectares - teachers +``` + +Write a function `aclean(arr)` that returns an array cleaned from anagrams. + +For instance: + +```js +let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; + +alert( aclean(arr) ); // "nap,teachers,ear" or "PAN,cheaters,era" +``` + +From every anagram group should remain only one word, no matter which one. + diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/solution.md b/1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/solution.md new file mode 100644 index 00000000..7310d1d3 --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/solution.md @@ -0,0 +1,19 @@ + +That's because `map.keys()` returns an iterable, but not an array. + +We can convert it into an array using `Array.from`: + + +```js run +let map = new Map(); + +map.set("name", "John"); + +*!* +let keys = Array.from(map.keys()); +*/!* + +keys.push("more"); + +alert(keys); // name, more +``` diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/task.md b/1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/task.md new file mode 100644 index 00000000..9a3e5d25 --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/task.md @@ -0,0 +1,24 @@ +importance: 5 + +--- + +# Iterable keys + +We want to get an array of `map.keys()` and go on working with it (apart from the map itself). + +But there's a problem: + +```js run +let map = new Map(); + +map.set("name", "John"); + +let keys = map.keys(); + +*!* +// Error: numbers.push is not a function +keys.push("more"); +*/!* +``` + +Why? How can we fix the code to make `keys.push` work? diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/solution.md b/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/solution.md new file mode 100644 index 00000000..ce56f593 --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/solution.md @@ -0,0 +1,41 @@ +The sane choice here is a `WeakSet`: + +```js +let messages = [ + {text: "Hello", from: "John"}, + {text: "How goes?", from: "John"}, + {text: "See you soon", from: "Alice"} +]; + +let readMessages = new WeakSet(); + +// two messages have been read +readMessages.add(messages[0]); +readMessages.add(messages[1]); +// readMessages has 2 elements + +// ...let's read the first message again! +readMessages.add(messages[0]); +// readMessages still has 2 unique elements + +// answer: was the message[0] read? +alert("Read message 0: " + readMessages.has(messages[0])); // true + +messages.shift(); +// now readMessages has 1 element (technically memory may be cleaned later) +``` + +The `WeakSet` allows to store a set of messages and easily check for the existance of a message in it. + +It cleans up itself automatically. The tradeoff is that we can't iterate over it. We can't get "all read messages" directly. But we can do it by iterating over all messages and filtering those that are in the set. + +P.S. Adding a property of our own to each message may be dangerous if messages are managed by someone else's code, but we can make it a symbol to evade conflicts. + +Like this: +```js +// the symbolic property is only known to our code +let isRead = Symbol("isRead"); +messages[0][isRead] = true; +``` + +Now even if someone else's code uses `for..in` loop for message properties, our secret flag won't appear. diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/task.md b/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/task.md new file mode 100644 index 00000000..7ec1faf1 --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/task.md @@ -0,0 +1,23 @@ +importance: 5 + +--- + +# Store "unread" flags + +There's an array of messages: + +```js +let messages = [ + {text: "Hello", from: "John"}, + {text: "How goes?", from: "John"}, + {text: "See you soon", from: "Alice"} +]; +``` + +Your code can access it, but the messages are managed by someone else's code. New messages are added, old ones are removed regularly by that code, and you don't know the exact moments when it happens. + +Now, which data structure you could use to store information whether the message "have been read"? The structure must be well-suited to give the answer "was it read?" for the given message object. + +P.S. When a message is removed from `messages`, it should disappear from your structure as well. + +P.P.S. We shouldn't modify message objects directly. If they are managed by someone else's code, then adding extra properties to them may have bad consequences. diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/solution.md b/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/solution.md new file mode 100644 index 00000000..7f387b4d --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/solution.md @@ -0,0 +1,15 @@ + +To store a date, we can use `WeakMap`: + +```js +let messages = [ + {text: "Hello", from: "John"}, + {text: "How goes?", from: "John"}, + {text: "See you soon", from: "Alice"} +]; + +let readMap = new WeakMap(); + +readMap.set(messages[0], new Date(2017, 1, 1)); +// Date object we'll study later +``` diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/task.md b/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/task.md new file mode 100644 index 00000000..22b51a38 --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/task.md @@ -0,0 +1,19 @@ +importance: 5 + +--- + +# Store read dates + +There's an array of messages as in the [previous task](info:task/recipients-read). The situation is similar. + +```js +let messages = [ + {text: "Hello", from: "John"}, + {text: "How goes?", from: "John"}, + {text: "See you soon", from: "Alice"} +]; +``` + +The question now is: which data structure you'd suggest to store the information: "when the message was read?". + +In the previous task we only needed to store the "yes/no" fact. Now we need to store the date and it, once again, should disappear if the message is gone. diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/article.md b/1-js/05-data-types/07-map-set-weakmap-weakset/article.md new file mode 100644 index 00000000..34347151 --- /dev/null +++ b/1-js/05-data-types/07-map-set-weakmap-weakset/article.md @@ -0,0 +1,432 @@ + +# Map, Set, WeakMap and WeakSet + +Now we know the following complex data structures: + +- Objects for storing keyed collections. +- Arrays for storing ordered collections. + +But that's not enough for real life. That's why there also exist `Map` and `Set`. + +## Map + +[Map](mdn:js/Map) is a collection of keyed data items. Just like an `Object`. But the main difference is that `Map` allows keys of any type. + +The main methods are: + +- `new Map()` -- creates the map. +- `map.set(key, value)` -- stores the value by the key. +- `map.get(key)` -- returns the value by the key. +- `map.has(key)` -- returns `true` if the `key` exists, `false` otherwise. +- `map.delete(key)` -- removes the value by the key. +- `map.clear()` -- clears the map +- `map.size` -- is the current elements count. + +For instance: + +```js run +let map = new Map(); + +map.set('1', 'str1'); // a string key +map.set(1, 'num1'); // a numeric key +map.set(true, 'bool1'); // a boolean key + +// remember the regular Object? it would convert keys to string +// Map keeps the type, so these two are different: +alert( map.get(1) ); // 'num1' +alert( map.get('1') ); // 'str1' + +alert( map.size ); // 3 +``` + +As we can see, unlike objects, keys are not converted to strings. Any type of key is possible. + +**Map can also use objects as keys.** + +For instance: +```js run +let john = { name: "John" }; + +// for every user, let's store his visits count +let visitsCountMap = new Map(); + +// john is the key for the map +visitsCountMap.set(john, 123); + +alert( visitsCountMap.get(john) ); // 123 +``` + +Using objects as keys is one of most notable and important `Map` features. For string keys, `Object` can be fine, but it would be difficult to replace the `Map` with a regular `Object` in the example above. + +In the old times, before `Map` existed, people added unique identifiers to objects for that: + +```js run +// we add the id field +let john = { name: "John", *!*id: 1*/!* }; + +let visitsCounts = {}; + +// now store the value by id +visitCounts[john.id] = 123; + +alert( visitsCounts[john.id] ); // 123 +``` + +...But `Map` is much more elegant. + + +```smart header="How `Map` compares keys" +To test values for equivalence, `Map` uses the algorithm [SameValueZero](https://tc39.github.io/ecma262/#sec-samevaluezero). It is roughly the same as the strict equality `===`, but the difference is that `NaN` is considered equal to `NaN`. So `NaN` can be used as the key as well. + +This algorithm can't be changed or customized. +``` + + +````smart header="Chaining" + +Every `map.set` call returns the map itself, so we can "chain" the calls: + +```js +map.set('1', 'str1') + .set(1, 'num1') + .set(true, 'bool1'); +``` +```` + +## Map from Object + +When a `Map` is created, we can pass an array (or another iterable) with key-value pairs, like this: + +```js +// array of [key, value] pairs +let map = new Map([ + ['1', 'str1'], + [1, 'num1'], + [true, 'bool1'] +]); +``` + +There is a built-in method [Object.entries(obj)](mdn:js/Object/entries) that returns the array of key/value pairs for an object exactly in that format. + +So we can initialize a map from an object like this: + +```js +let map = new Map(Object.entries({ + name: "John", + age: 30 +})); +``` + +Here, `Object.entries` returns the array of key/value pairs: `[ ["name","John"], ["age", 30] ]`. That's what `Map` needs. + +## Iteration over Map + +For looping over a `map`, there are 3 methods: + +- `map.keys()` -- returns an iterable for keys, +- `map.values()` -- returns an iterable for values, +- `map.entries()` -- returns an iterable for entries `[key, value]`, it's used by default in `for..of`. + +For instance: + +```js run +let recipeMap = new Map([ + ['cucumber', 500], + ['tomatoes', 350], + ['onion', 50] +]); + +// iterate over keys (vegetables) +for(let vegetable of recipeMap.keys()) { + alert(vegetable); // cucumber, tomateos, onion +} + +// iterate over values (amounts) +for(let amount of recipeMap.values()) { + alert(amount); // 500, 350, 50 +} + +// iterate over [key, value] entries +for(let entry of recipeMap) { // the same as of recipeMap.entries() + alert(entry); // cucumber,50 (and so on) +} +``` + +```smart header="The insertion order is used" +The iteration goes in the same order as the values were inserted. `Map` preserves this order, unlike a regular `Object`. +``` + +Besides that, `Map` has a built-in `forEach` method, similar to `Array`: + +```js +recipeMap.forEach( (value, key, map) => { + alert(`${key}: ${value}`); // cucumber: 50 etc +}); +``` + + +## Set + +`Set` -- is a collection of values, where each value may occur only once. + +The main methods are: + +- `new Set(iterable)` -- creates the set, optionally from an array of values (any iterable will do). +- `set.add(value)` -- adds a value, returns the set itself. +- `set.delete(value)` -- removes the value, returns `true` if `value` existed at the moment of the call, otherwise `false`. +- `set.has(value)` -- returns `true` if the value exists in the set, otherwise `false`. +- `set.clear()` -- removes everything from the set. +- `set.size` -- is the elements count. + +For example, we have visitors coming, and we'd like to remember everyone. But repeated visits should not lead to duplicates. A visitor must be "counted" only once. + +`Set` is just the right thing for that: + +```js run +let set = new Set(); + +let john = { name: "John" }; +let pete = { name: "Pete" }; +let mary = { name: "Mary" }; + +// visits, some users come multiple times +set.add(john); +set.add(pete); +set.add(mary); +set.add(john); +set.add(mary); + +// set keeps only unique values +alert( set.size ); // 3 + +for(let user of set) { + alert(user.name); // John (then Pete and Mary) +} +``` + +The alternative to `Set` could be an array of users, and the code to check for duplicates on every insertion using [arr.find](mdn:js/Array/find). But the performance would be much worse, because this method walks through the whole array checking every element. `Set` is much better optimized internally for uniqueness checks. + +## Iteration over Set + +We can loop over a set either with `for..of` or using `forEach`: + +```js run +let set = new Set(["oranges", "apples", "bananas"]); + +for(let value of set) alert(value); + +// the same with forEach: +set.forEach((value, valueAgain, set) => { + alert(value); +}); +``` + +Note the funny thing. The `forEach` function in the `Set` has 3 arguments: a value, then *again a value*, and then the target object. Indeed, the same value appears in the arguments twice. + +That's made for compatibility with `Map` where `forEach` has three arguments. + +The same methods as `Map` has for iterators are also supported: + +- `set.keys()` -- returns an iterable object for values, +- `set.values()` -- same as `set.keys`, for compatibility with `Map`, +- `set.entries()` -- returns an iterable object for entries `[value, value]`, exists for compatibility with `Map`. + +## WeakMap and WeakSet + +`WeakSet` is a special kind of `Set` that does not prevent JavaScript from removing its items from memory. `WeakMap` is the same thing for `Map`. + +As we know from the chapter , JavaScript engine stores a value in memory while it is reachable (and can potentially be used). + +For instance: +```js +let john = { name: "John" }; + +// the object can be accessed, john is the reference to it + +// overwrite the reference +john = null; + +*!* +// the object will be removed from memory +*/!* +``` + +Usually, properties of an object or elements of an array or another data structure are considered reachable and kept in memory while that data structure is in memory. + +In a regular `Map`, it does not matter if we store an object as a key or as a value. It's kept in memory even if there are no more references to it. + +For instance: +```js +let john = { name: "John" }; + +let map = new Map(); +map.set(john, "..."); + +john = null; // overwrite the reference + +*!* +// john is stored inside the map +// we can get it by using map.keys() +*/!* +``` + + +With the exception of `WeakMap/WeakSet`. + +**`WeakMap/WeakSet` does not prevent the object removal from the memory.** + +Let's start with `WeakMap`. + +The first difference from `Map` is that its keys must be objects, not primitive values: + +```js run +let weakMap = new WeakMap(); + +let obj = {}; + +weakMap.set(obj, "ok"); // works fine (object key) + +*!* +weakMap.set("test", "Whoops"); // Error, because "test" is a primitive +*/!* +``` + +Now, if we use an object as the key in it, and there are no other references to that object -- it will be removed from memory (and from the map) automatically. + +```js +let john = { name: "John" }; + +let weakMap = new WeakMap(); +weakMap.set(john, "..."); + +john = null; // overwrite the reference + +// john is removed from memory! +``` + +Compare it with the regular `Map` example above. Now if `john` only exists as the key of `WeakMap` -- it is to be automatically deleted. + +...And `WeakMap` does not support methods `keys()`, `values()`, `entries()`, we can not iterate over it. So there's really no way to receive all keys or values from it. + +`WeakMap` has only the following methods: + +- `weakMap.get(key)` +- `weakMap.set(key, value)` +- `weakMap.delete(key, value)` +- `weakMap.has(key)` + +Why such a limitation? That's for technical reasons. If the object has lost all other references (like `john` in the code above), then it is to be deleted automatically. But technically it's not exactly specified *when the cleanup happens*. + +The JavaScript engine decides that. It may choose to perform the memory cleanup immediately or to wait and do the cleaning later when more deletions happen. So, technically the current element count of the `WeakMap` is not known. The engine may have cleaned it up or not, or did it partially. For that reason, methods that access `WeakMap` as a whole are not supported. + +Now where do we need such thing? + +The idea of `WeakMap` is that we can store something for an object that exists only while the object exists. But we do not force the object to live by the mere fact that we store something for it. + +```js +weakMap.put(john, "secret documents"); +// if john dies, secret documents will be destroyed +``` + +That's useful for situations when we have a main storage for the objects somewhere and need to keep additional information that is only relevant while the object lives. + +Let's see an example. + +For instance, we have a code that keeps a visit count for each user. The information is stored in a map: a user is the key and the visit count is the value. When a user leaves, we don't want to store his visit count any more. + +One way would be to keep track of leaving users and clean up the storage manually: + +```js run +let john = { name: "John" }; + +// map: user => visits count +let visitsCountMap = new Map(); + +// john is the key for the map +visitsCountMap.set(john, 123); + +// now john leaves us, we don't need him any more +john = null; + +*!* +// but it's still in the map, we need to clean it! +*/!* +alert( visitsCountMap.size ); // 1 +// it's also in the memory, because Map uses it as the key +``` + +Another way would be to use `WeakMap`: + +```js +let john = { name: "John" }; + +let visitsCountMap = new WeakMap(); + +visitsCountMap.set(john, 123); + +// now john leaves us, we don't need him any more +john = null; + +// there are no references except WeakMap, +// so the object is removed both from the memory and from visitsCountMap automatically +``` + +With a regular `Map`, cleaning up after a user has left becomes a tedious task: we not only need to remove the user from its main storage (be it a variable or an array), but also need to clean up the additional stores like `visitsCountMap`. And it can become cumbersome in more complex cases when users are managed in one place of the code and the additional structure is at another place and is getting no information about removals. + +`WeakMap` can make things simpler, because it is cleaned up automatically. The information in it like visits count in the example above lives only while the key object exists. + +`WeakSet` behaves similarly: + +- It is analogous to `Set`, but we may only add objects to `WeakSet` (not primitives). +- An object exists in the set while it has reachable from somewhere else. +- Like `Set`, it supports `add`, `has` and `delete`, but not `size`, `keys()` and no iterations. + +For instance, we can use it to keep track of whether an item is checked: + +```js +let messages = [ + {text: "Hello", from: "John"}, + {text: "How goes?", from: "John"}, + {text: "See you soon", from: "Alice"} +]; + +// fill it with array elements (3 items) +let unreadSet = new WeakSet(messages); + +// we can use unreadSet to see whether a message is unread +alert(unreadSet.has(messages[1])); // true +// remove it from the set after reading +unreadSet.delete(messages[1]); // true + +// and when we shift our messages history, the set is cleaned up automatically +messages.shift(); +// no need to clean unreadSet, it now has 2 items +// unfortunately, there's no method to get the exact count of items, so can't show it +``` + +The most notable limitation of `WeakMap` and `WeakSet` is the absence of iterations, and inability to get all current content. That may appear inconvenient, but actually does not prevent `WeakMap/WeakSet` from doing their main job -- be an "additional" storage of data for objects which are stored/managed at another place. + +## Summary + +- `Map` -- is a a collection of keyed values. + + The differences from a regular `Object`: + + - Any keys, objects can be keys. + - Iterates in the insertion order. + - Additional convenient methods, the `size` property. + +- `Set` -- is a collection of unique values. + + - Unlike an array, does not allow to reorder elements. + - Keeps the insertion order. + +- `WeakMap` -- a variant of `Map` that allows only objects as keys and removes them once they become unaccessible by other means. + + - It does not support operations on the structure as a whole: no `size`, no `clear()`, no iterations. + +- `WeakSet` -- is a variant of `Set` that only stores objects and removes them once they become unaccessible by other means. + + - Also does not support `size/clear()` and iterations. + +`WeakMap` and `WeakSet` are used as "secondary" data structures in additional to the "main" object storage. Once the object is removed from the main storage, so it only stays in `WeakMap/WeakSet`, they clean up aumatically. diff --git a/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/solution.js b/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/solution.js new file mode 100644 index 00000000..509b3589 --- /dev/null +++ b/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/solution.js @@ -0,0 +1,10 @@ +function sumSalaries(salaries) { + + let sum = 0; + for (let salary of Object.values(salaries)) { + sum += salary; + } + + return sum; +} + diff --git a/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/test.js b/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/test.js new file mode 100644 index 00000000..684b0894 --- /dev/null +++ b/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/test.js @@ -0,0 +1,15 @@ +describe("sumSalaries", function() { + it("returns sum of salaries", function() { + let salaries = { + "John": 100, + "Pete": 300, + "Mary": 250 + }; + + assert.equal( sumSalaries(salaries), 650 ); + }); + + it("returns 0 for the empty object", function() { + assert.strictEqual( sumSalaries({}), 0); + }); +}); \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/6-behavior/1-behavior-tooltip/solution.md b/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/solution.md similarity index 100% rename from 2-ui/2-events-and-interfaces/6-behavior/1-behavior-tooltip/solution.md rename to 1-js/05-data-types/08-keys-values-entries/01-sum-salaries/solution.md diff --git a/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/task.md b/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/task.md new file mode 100644 index 00000000..211357d0 --- /dev/null +++ b/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/task.md @@ -0,0 +1,24 @@ +importance: 5 + +--- + +# Sum the properties + +There is a `salaries` object with arbitrary number of salaries. + +Write the function `sumSalaries(salaries)` that returns the sum of all salaries using `Object.values` and the `for..of` loop. + +If `salaries` is empty, then the result must be `0`. + +For instance: + +```js +let salaries = { + "John": 100, + "Pete": 300, + "Mary": 250 +}; + +alert( sumSalaries(salaries) ); // 650 +``` + diff --git a/1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/solution.js b/1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/solution.js new file mode 100644 index 00000000..1853d20e --- /dev/null +++ b/1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/solution.js @@ -0,0 +1,4 @@ +function count(obj) { + return Object.keys(obj).length; +} + diff --git a/1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/test.js b/1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/test.js new file mode 100644 index 00000000..e568c320 --- /dev/null +++ b/1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/test.js @@ -0,0 +1,13 @@ +describe("count", function() { + it("counts the number of properties", function() { + assert.equal( count({a: 1, b: 2}), 2 ); + }); + + it("returns 0 for an empty object", function() { + assert.equal( count({}), 0 ); + }); + + it("ignores symbolic properties", function() { + assert.equal( count({ [Symbol('id')]: 1 }), 0 ); + }); +}); \ No newline at end of file diff --git a/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/solution.md b/1-js/05-data-types/08-keys-values-entries/02-count-properties/solution.md similarity index 100% rename from 2-ui/3-event-details/1-mouse-clicks/1-selectable-list/solution.md rename to 1-js/05-data-types/08-keys-values-entries/02-count-properties/solution.md diff --git a/1-js/05-data-types/08-keys-values-entries/02-count-properties/task.md b/1-js/05-data-types/08-keys-values-entries/02-count-properties/task.md new file mode 100644 index 00000000..d7aebb1f --- /dev/null +++ b/1-js/05-data-types/08-keys-values-entries/02-count-properties/task.md @@ -0,0 +1,21 @@ +importance: 5 + +--- + +# Count properties + +Write a function `count(obj)` that returns the number of properties in the object: + +```js +let user = { + name: 'John', + age: 30 +}; + +alert( count(user) ); // 2 +``` + +Try to make the code as short as possible. + +P.S. Ignore symbolic properties, count only "regular" ones. + diff --git a/1-js/05-data-types/08-keys-values-entries/article.md b/1-js/05-data-types/08-keys-values-entries/article.md new file mode 100644 index 00000000..f18c065b --- /dev/null +++ b/1-js/05-data-types/08-keys-values-entries/article.md @@ -0,0 +1,70 @@ + +# Object.keys, values, entries + +Let's step away from the indivitual data structures and talk about the iterations over them. + +In the previous chapter we saw methods `map.keys()`, `map.values()`, `map.entries()`. + +These methods are generic, there is a common agreement to use them for data structures. If we ever create a data structure of our own, we should implement them too. + +They are supported for: + +- `Map` +- `Set` +- `Array` (except `arr.values()`) + +Plain objects also support similar methods, but the syntax is a bit different. + +## Object.keys, values, entries + +For plain objects, the following methods are available: + +- [Object.keys(obj)](mdn:js/Object/keys) -- returns an array of keys. +- [Object.values(obj)](mdn:js/Object/values) -- returns an array of values. +- [Object.entries(obj)](mdn:js/Object/entries) -- returns an array of `[key, value]` pairs. + +...But please note the distinctions (compared to map for example): + +| | Map | Object | +|-------------|------------------|--------------| +| Call syntax | `map.keys()` | `Object.keys(obj)`, but not `obj.keys()` | +| Returns | iterable | "real" Array | + +The first difference is that we have to call `Object.keys(obj)`, and not `obj.keys()`. + +Why so? There main reason is flexibility. Remember, objects are a base of all complex structures in JavaScript. So we may have an object of our own like `order` that implements its own `order.values()` method. And we still can call `Object.values(order)` on it. + +The second difference is that `Object.*` methods return "real" array objects, not just an iterable. That's mainly for historical reasons. + +For instance: + +```js +let user = { + name: "John", + age: 30 +}; +``` + +- `Object.keys(user) = [name, age]` +- `Object.values(user) = ["John", 30]` +- `Object.entries(user) = [ ["name","John"], ["age",30] ]` + +Here's an example of using `Object.values` to loop over property values: + +```js run +let user = { + name: "John", + age: 30 +}; + +// loop over values +for(let value of Object.values(user)) { + alert(value); // John, then 30 +} +``` + +```smart header="`Object.keys/values/entries` ignore symbolic properties" +Just like `for..in` loop, these methods ignore properties that use `Symbol(...)` as keys. + +Usually that's convenient. But if we want symbolic keys too, then there's a separate method [Object.getOwnPropertySymbols](mdn:js/Object/getOwnPropertySymbols) that returns an array of only symbolic keys. Also, the method [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) returns *all* keys. +``` diff --git a/1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment/solution.md b/1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment/solution.md new file mode 100644 index 00000000..cc226e7c --- /dev/null +++ b/1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment/solution.md @@ -0,0 +1,13 @@ + +```js run +let user = { + name: "John", + years: 30 +}; + +let {name, years: age, isAdmin = false} = user; + +alert( name ); // John +alert( age ); // 30 +alert( isAdmin ); // false +``` \ No newline at end of file diff --git a/1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment/task.md b/1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment/task.md new file mode 100644 index 00000000..b2213323 --- /dev/null +++ b/1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment/task.md @@ -0,0 +1,33 @@ +importance: 5 + +--- + +# Destructuring assignment + +We have an object: + +```js +let user = { + name: "John", + years: 30 +}; +``` + +Write the destructuring assignment that reads: + +- `name` property into the variable `name`. +- `years` property into the variable `age`. +- `isAdmin` property into the variable `isAdmin` (false if absent) + +The values after the assignment should be: + +```js +let user = { name: "John", years: 30 }; + +// your code to the left side: +// ... = user + +alert( name ); // John +alert( age ); // 30 +alert( isAdmin ); // false +``` diff --git a/1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/solution.js b/1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/solution.js new file mode 100644 index 00000000..d95cf1b1 --- /dev/null +++ b/1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/solution.js @@ -0,0 +1,16 @@ +function topSalary(salaries) { + + let max = 0; + let maxName = null; + + for(let [name, salary] of Object.entries(salaries)) { + if (max < salary) { + max = salary; + maxName = name; + } + } + + return maxName; +} + + diff --git a/1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/test.js b/1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/test.js new file mode 100644 index 00000000..e1da754b --- /dev/null +++ b/1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/test.js @@ -0,0 +1,15 @@ +describe("topSalary", function() { + it("returns top-paid person", function() { + let salaries = { + "John": 100, + "Pete": 300, + "Mary": 250 + }; + + assert.equal( topSalary(salaries), "Pete" ); + }); + + it("returns null for the empty object", function() { + assert.isNull( topSalary({}) ); + }); +}); \ No newline at end of file diff --git a/2-ui/3-event-details/6-mousewheel/2-no-doc-scroll/solution.md b/1-js/05-data-types/09-destructuring-assignment/6-max-salary/solution.md similarity index 100% rename from 2-ui/3-event-details/6-mousewheel/2-no-doc-scroll/solution.md rename to 1-js/05-data-types/09-destructuring-assignment/6-max-salary/solution.md diff --git a/1-js/05-data-types/09-destructuring-assignment/6-max-salary/task.md b/1-js/05-data-types/09-destructuring-assignment/6-max-salary/task.md new file mode 100644 index 00000000..9f33de08 --- /dev/null +++ b/1-js/05-data-types/09-destructuring-assignment/6-max-salary/task.md @@ -0,0 +1,22 @@ +importance: 5 + +--- + +# The maximal salary + +There is a `salaries` object: + +```js +let salaries = { + "John": 100, + "Pete": 300, + "Mary": 250 +}; +``` + +Create the function `topSalary(salaries)` that returns the name of the top-paid person. + +- If `salaries` is empty, it should return `null`. +- If there are multiple top-paid persons, return any of them. + +P.S. Use `Object.entries` and destructuring to iterate over key/value pairs. diff --git a/1-js/05-data-types/09-destructuring-assignment/article.md b/1-js/05-data-types/09-destructuring-assignment/article.md new file mode 100644 index 00000000..701deb06 --- /dev/null +++ b/1-js/05-data-types/09-destructuring-assignment/article.md @@ -0,0 +1,517 @@ +# Destructuring assignment + +The two most used data structures in JavaScript are `Object` and `Array`. + +Objects allow to pack many pieces of information into a single entity and arrays allow to store ordered collections. So we can make an object or an array and handle it as a single thing, maybe pass to a function call. + +*Destructuring assignment* is a special syntax that allows to "unpack" arrays or objects into a bunch of variables, as sometimes they are more convenient. Destructuring also works great with complex functions that have a lot of parameters, default values, and soon we'll see how these are handled too. + +[cut] + +## Array destructuring + +An example of how the array is destructured into variables: + +```js +// we have an array with the name and surname +let arr = ["Ilya", "Kantor"] + +*!* +// destructuring assignment +let [firstName, surname] = arr; +*/!* + +alert(firstName); // Ilya +alert(surname); // Kantor +``` + +Now we can work with variables instead of array members. + +It looks great when combined with `split` or other array-returning methods: + +```js +let [firstName, surname] = "Ilya Kantor".split(' '); +``` + +````smart header="\"Destructuring\" does not mean \"destructive\"" +That's called "destructuring assignment", because it "destructurizes" by copying items into variables. But the array itself is not modified. + +That's just a shorter way to write: +```js +// let [firstName, surname] = arr; +let firstName = arr[0]; +let surname = arr[1]; +``` +```` + +````smart header="Ignore first elements" +Unwanted elements of the array can also be thrown away via an extra comma: + +```js run +*!* +// first and second elements are not needed +let [, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; +*/!* + +alert( title ); // Imperator +``` + +In the code above, the first and second elements of the array are skipped, the third one is assigned to `title`, and the rest is also skipped. +```` + +````smart header="Works with any iterable on the right-side" + +...Actually, we can use it with any iterable, not only an array: + +```js +let [a, b, c] = "abc"; // ["a", "b", "c"] +let [one, two, three] = new Set([1, 2, 3]); +``` + +```` + + +````smart header="Assign to anything at the left-side" + +We can use any "assignables" at the left side. + +For instance, an object property: +```js run +let user = {}; +[user.name, user.surname] = "Ilya Kantor".split(' '); + +alert(user.name); // Ilya +``` + +```` + +````smart header="Looping with .entries()" + +In the previous chapter we saw [Object.entries(obj)](mdn:js/Object/entries) method. + +We can use it with destructuring to loop over keys-and-values of an object: + +```js run +let user = { + name: "John", + age: 30 +}; + +// loop over keys-and-values +*!* +for(let [key, value] of Object.entries(user)) { +*/!* + alert(`${key}:${value}`); // name:John, then age:30 +} +``` + +...And the same for a map: + +```js run +let user = new Map(); +user.set("name", "John"); +user.set("age", "30"); + +*!* +for(let [key, value] of user.entries()) { +*/!* + alert(`${key}:${value}`); // name:John, then age:30 +} +``` +```` +### The rest '...' + +If we want not just to get first values, but also to gather all that follows -- we can add one more parameter that gets "the rest" using the three dots `"..."`: + +```js run +let [name1, name2, *!*...rest*/!*] = ["Julius", "Caesar", *!*"Consul", "of the Roman Republic"*/!*]; + +alert(name1); // Julius +alert(name2); // Caesar + +*!* +alert(rest[0]); // Consul +alert(rest[1]); // of the Roman Republic +alert(rest.length); // 2 +*/!* +``` + +The value of `rest` is the array of the remaining array elements. We can use any other variable name in place of `rest`, just make sure it has three dots before it and goes the last. + +### Default values + +If there are fewer values in the array than variables in the assignment -- there will be no error, absent values are considered undefined: + +```js run +*!* +let [firstName, surname] = []; +*/!* + +alert(firstName); // undefined +``` + +If we want a "default" value to replace the missing one, we can provide it using `=`: + +```js run +*!* +// default values +let [name="Guest", surname="Anonymous"] = ["Julius"]; +*/!* + +alert(name); // Julius (from array) +alert(surname); // Anonymous (default used) +``` + +Default values can be more complex expressions or even function calls. They are evaluated only if the value is not provided. + +For instance, here we use `prompt` function for two defaults. But it will only run only for the missing one: + +```js run +// runs only prompt for surname +let [name=prompt('name?'), surname=prompt('surname?')] = ["Julius"]; + +alert(name); // Julius (from array) +alert(surname); // whatever prompt gets +``` + + + +## Object destructuring + +The destructuring assignment also works with objects. + +The basic syntax is: + +```js +let {var1, var2} = {var1:…, var2…} +``` + +We have an existing object at the right side, that we want to split into variables. To the left side contains a "pattern" for corresponding properties. In the simple case that's a list of variable names in `{...}`. + +For instance: + +```js run +let options = { + title: "Menu", + width: 100, + height: 200 +}; + +*!* +let {title, width, height} = options; +*/!* + +alert(title); // Menu +alert(width); // 100 +alert(height); // 200 +``` + +Properties `options.title`, `options.width` and `options.height` are assigned to the corresponding variables. The order does not matter, that works too: + +```js +// changed the order of properties in let {...} +let {height, width, title} = { title: "Menu", height: 200, width: 100 } +``` + +The pattern on the left side may be more complex and specify the mapping between properties and variables. + +If we want to assign a property to a variable with another name, for instance, `options.width` to go into the variable named `w`, then we can set it using a colon: + +```js run +let options = { + title: "Menu", + width: 100, + height: 200 +}; + +*!* +// { sourceProperty: targetVariable } +let {width: w, height: h, title} = options; +*/!* + +// width -> w +// height -> h +// title -> title + +alert(title); // Menu +alert(w); // 100 +alert(h); // 200 +``` + +The colon shows "what : goes where". In the example above the property `width` goes to `w`, property `height` goes to `h`, and `title` is assigned to the same name. + +For potentially missing properties we can set default values using `"="`, like this: + +```js run +let options = { + title: "Menu" +}; + +*!* +let {width=100, height=200, title} = options; +*/!* + +alert(title); // Menu +alert(width); // 100 +alert(height); // 200 +``` + +Just like with arrays or function parameters, default values can be any expressions or even function calls. They will be evaluated if the value is not provided. + +The code below asks for width, but not about the title. + +```js run +let options = { + title: "Menu" +}; + +*!* +let {width=prompt("width?"), title=prompt("title?")} = options; +*/!* + +alert(title); // Menu +alert(width); // (whatever you the result of prompt is) +``` + +We also can combine both the colon and equality: + +```js run +let options = { + title: "Menu" +}; + +*!* +let {width:w=100, height:h=200, title} = options; +*/!* + +alert(title); // Menu +alert(w); // 100 +alert(h); // 200 +``` + +### The rest operator + +What if the object has more properties than we have variables? Can we take some and then assign the "rest" somewhere? + +The specification for using the rest operator (three dots) here is almost in the standard, but most browsers do not support it yet. + +It looks like this: + +```js run +let options = { + title: "Menu", + height: 200, + width: 100 +}; + +*!* +let {title, ...rest} = options; +*/!* + +// now title="Menu", rest={height: 200, widht: 100} +alert(rest.height); // 200 +alert(rest.width); // 100 +``` + + + +````smart header="Gotcha without `let`" +In the examples above variables were declared right before the assignment: `let {…} = {…}`. Of course, we could use existing variables too. But there's a catch. + +This won't work: +```js run +let title, width, height; + +// error in this line +{title, width, height} = {title: "Menu", width: 200, height: 100}; +``` + +The problem is that JavaScript treats `{...}` in the main code flow (not inside another expression) as a code block. Such code blocks can be used to group statements, like this: + +```js run +{ + // a code block + let message = "Hello"; + // ... + alert( message ); +} +``` + +To show JavaScript that it's not a code block, we can wrap the whole assignment in brackets `(...)`: + +```js run +let title, width, height; + +// okay now +*!*(*/!*{title, width, height} = {title: "Menu", width: 200, height: 100}*!*)*/!*; + +alert( title ); // Menu +``` + +```` + +## Nested destructuring + +If an object or an array contain other objects and arrays, we can use more complex left-side patterns to extract deeper portions. + +In the code below `options` has another object in the property `size` and an array in the property `items`. The pattern at the left side of the assignment has the same structure: + +```js run +let options = { + size: { + width: 100, + height: 200 + }, + items: ["Cake", "Donut"], + extra: true // something extra that we will not destruct +}; + +// destructuring assignment on multiple lines for clarity +let { + size: { // put size here + width, + height + }, + items: [item1, item2], // assign items here + title = "Menu" // not present in the object (default value is used) +} = options; + +alert(title); // Menu +alert(width); // 100 +alert(height); // 200 +alert(item1); // Cake +alert(item2); // Donut +``` + +The whole `options` object except `extra` that was not mentioned, is assigned to corresponding variables. + +![](destructuring-complex.png) + +Finally, we have `width`, `height`, `item1`, `item2` and `title` from the default value. + +That often happens with destructuring assignments. We have a complex object with may properties and want extract only what we need. + +Even like this: +```js +// take size as a whole into a variable, ignore the rest +let { size } = options; +``` + +## Smart function parameters + +There are times when a function may have many parameters, most of which are optional. That's especially true for user interfaces. Imagine a function that creates a menu. It may have a width, a height, a title, items list and so on. + +Here's a bad way to write such function: + +```js +function showMenu(title = "Untitled", width = 200, height = 100, items = []) { + // ... +} +``` + +The real-life problem is how to remember the order of arguments. Usually IDEs try to help us, especially if the code is well-documented, but still... Another problem is how to call a function when most parameters are ok by default. + +Like this? + +```js +showMenu("My Menu", undefined, undefined, ["Item1", "Item2"]) +``` + +That's ugly. And becomes unreadable when we deal with more parameters. + +Destructuring comes to the rescue! + +We can pass parameters as an object, and the function immediately destructurizes them into variables: + +```js run +// we pass object to function +let options = { + title: "My menu", + items: ["Item1", "Item2"] +}; + +// ...and it immediately expands it to variables +function showMenu(*!*{title = "Untitled", width = 200, height = 100, items = []}*/!*) { + // title, items – taken from options, + // width, height – defaults used + alert( title + ' ' + width + ' ' + height ); // My Menu 100 200 + alert( items ); // Item1, Item2 +} + +showMenu(options); +``` + +We can also use more complex destructuring with nested objects and colon mappings: + +```js run +let options = { + title: "My menu", + items: ["Item1", "Item2"] +}; + +*!* +function showMenu({ + title = "Untitled", + width:w = 100, // width goes to w + height:h = 200, // height goes to h + items: [item1, item2] // items first element goes to item1, second to item2 +}) { +*/!* + alert( title + ' ' + w + ' ' + h ); // My Menu 100 200 + alert( item1 ); // Item1 + alert( item2 ); // Item2 +} + +showMenu(options); +``` + +The syntax is the same as for a destructuring assignment: +```js +function({ + incomingProperty: parameterName = defaultValue + ... +}) +``` + +Please note that such destructuring assumes that `showMenu()` does have an argument. If we want all values by default, then we should specify an empty object: + +```js +showMenu({}); + +// that would give an error +showMenu(); +``` + +We can fix this by making `{}` the default value for the whole destructuring thing: + + +```js run +// simplified parameters a bit for clarity +function showMenu(*!*{ title="Menu", width=100, height=200 } = {}*/!*) { + alert( title + ' ' + width + ' ' + height ); +} + +showMenu(); // Menu 100 200 +``` + +In the code above, the whole arguments object is `{}` by default, so there's always something to destructurize. + +## Summary + +- Destructuring assignment allows to instantly map an object or array into many variables. +- The object syntax: + ```js + let {prop : varName = default, ...} = object + ``` + + This means that property `prop` should go into the variable `varName` and, if no such property exists, then `default` value should be used. + +- The array syntax: + + ```js + let [item1 = default, item2, ...rest] = array + ``` + + The first item goes to `item1`, the second goes into `item2`, all the rest makes the array `rest`. + +- For more complex cases, the left side must have the same structure as the right one. diff --git a/1-js/05-data-types/09-destructuring-assignment/destructuring-complex.png b/1-js/05-data-types/09-destructuring-assignment/destructuring-complex.png new file mode 100644 index 00000000..50c4ffc9 Binary files /dev/null and b/1-js/05-data-types/09-destructuring-assignment/destructuring-complex.png differ diff --git a/1-js/05-data-types/09-destructuring-assignment/destructuring-complex@2x.png b/1-js/05-data-types/09-destructuring-assignment/destructuring-complex@2x.png new file mode 100644 index 00000000..bb908281 Binary files /dev/null and b/1-js/05-data-types/09-destructuring-assignment/destructuring-complex@2x.png differ diff --git a/1-js/05-data-types/10-date/1-new-date/solution.md b/1-js/05-data-types/10-date/1-new-date/solution.md new file mode 100644 index 00000000..eb271a91 --- /dev/null +++ b/1-js/05-data-types/10-date/1-new-date/solution.md @@ -0,0 +1,8 @@ +The `new Date` constructor uses the local time zone by default. So the only important thing to remember is that months start from zero. + +So February has number 1. + +```js run +let d = new Date(2012, 1, 20, 3, 12); +alert( d ); +``` diff --git a/1-js/05-data-types/10-date/1-new-date/task.md b/1-js/05-data-types/10-date/1-new-date/task.md new file mode 100644 index 00000000..1b40d5ac --- /dev/null +++ b/1-js/05-data-types/10-date/1-new-date/task.md @@ -0,0 +1,9 @@ +importance: 5 + +--- + +# Create a date + +Create a `Date` object for the date: Feb 20, 2012, 3:12am. The time zone is local. + +Show it using `alert`. diff --git a/1-js/05-data-types/10-date/2-get-week-day/_js.view/solution.js b/1-js/05-data-types/10-date/2-get-week-day/_js.view/solution.js new file mode 100644 index 00000000..642c376a --- /dev/null +++ b/1-js/05-data-types/10-date/2-get-week-day/_js.view/solution.js @@ -0,0 +1,5 @@ +function getWeekDay(date) { + let days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']; + + return days[date.getDay()]; +} diff --git a/1-js/05-data-types/10-date/2-get-week-day/_js.view/test.js b/1-js/05-data-types/10-date/2-get-week-day/_js.view/test.js new file mode 100644 index 00000000..3cdc9183 --- /dev/null +++ b/1-js/05-data-types/10-date/2-get-week-day/_js.view/test.js @@ -0,0 +1,29 @@ +describe("getWeekDay", function() { + it("3 January 2014 - friday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 3)), 'FR'); + }); + + it("4 January 2014 - saturday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 4)), 'SA'); + }); + + it("5 January 2014 - sunday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 5)), 'SU'); + }); + + it("6 January 2014 - monday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 6)), 'MO'); + }); + + it("7 January 2014 - tuesday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 7)), 'TU'); + }); + + it("8 January 2014 - wednesday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 8)), 'WE'); + }); + + it("9 January 2014 - thursday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 9)), 'TH'); + }); +}); diff --git a/1-js/05-data-types/10-date/2-get-week-day/solution.md b/1-js/05-data-types/10-date/2-get-week-day/solution.md new file mode 100644 index 00000000..2acaabf9 --- /dev/null +++ b/1-js/05-data-types/10-date/2-get-week-day/solution.md @@ -0,0 +1,14 @@ +The method `date.getDay()` returns the number of the weekday, starting from sunday. + +Let's make an array of weekdays, so that we can get the proper day name by its number: + +```js run +function getWeekDay(date) { + let days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']; + + return days[date.getDay()]; +} + +let date = new Date(2014, 0, 3); // 3 Jan 2014 +alert( getWeekDay(date) ); // FR +``` diff --git a/1-js/05-data-types/10-date/2-get-week-day/task.md b/1-js/05-data-types/10-date/2-get-week-day/task.md new file mode 100644 index 00000000..5cf31565 --- /dev/null +++ b/1-js/05-data-types/10-date/2-get-week-day/task.md @@ -0,0 +1,14 @@ +importance: 5 + +--- + +# Show a weekday + +Write a function `getWeekDay(date)` to show the weekday in short format: 'MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'. + +For instance: + +```js no-beautify +let date = new Date(2012, 0, 3); // 3 Jan 2012 +alert( getWeekDay(date) ); // should output "TU" +``` diff --git a/1-js/05-data-types/10-date/3-weekday/_js.view/solution.js b/1-js/05-data-types/10-date/3-weekday/_js.view/solution.js new file mode 100644 index 00000000..fb9e3d2a --- /dev/null +++ b/1-js/05-data-types/10-date/3-weekday/_js.view/solution.js @@ -0,0 +1,10 @@ +function getLocalDay(date) { + + let day = date.getDay(); + + if (day == 0) { // weekday 0 (sunday) is 7 in european + day = 7; + } + + return day; +} diff --git a/1-js/05-data-types/10-date/3-weekday/_js.view/test.js b/1-js/05-data-types/10-date/3-weekday/_js.view/test.js new file mode 100644 index 00000000..cdf0f535 --- /dev/null +++ b/1-js/05-data-types/10-date/3-weekday/_js.view/test.js @@ -0,0 +1,29 @@ +describe("getLocalDay returns the \"european\" weekday", function() { + it("3 January 2014 - friday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 3)), 5); + }); + + it("4 January 2014 - saturday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 4)), 6); + }); + + it("5 January 2014 - sunday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 5)), 7); + }); + + it("6 January 2014 - monday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 6)), 1); + }); + + it("7 January 2014 - tuesday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 7)), 2); + }); + + it("8 January 2014 - wednesday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 8)), 3); + }); + + it("9 January 2014 - thursday", function() { + assert.equal(getWeekDay(new Date(2014, 0, 9)), 4); + }); +}); diff --git a/1-js/05-data-types/10-date/3-weekday/solution.md b/1-js/05-data-types/10-date/3-weekday/solution.md new file mode 100644 index 00000000..bfe3f4ca --- /dev/null +++ b/1-js/05-data-types/10-date/3-weekday/solution.md @@ -0,0 +1,14 @@ +```js run +function getLocalDay(date) { + + let day = date.getDay(); + + if (day == 0) { // 0 becomes 7 + day = 7; + } + + return day; +} + +alert( getLocalDay(new Date(2012, 0, 3)) ); // 2 +``` diff --git a/1-js/05-data-types/10-date/3-weekday/task.md b/1-js/05-data-types/10-date/3-weekday/task.md new file mode 100644 index 00000000..c2ae2168 --- /dev/null +++ b/1-js/05-data-types/10-date/3-weekday/task.md @@ -0,0 +1,12 @@ +importance: 5 + +--- + +# European weekday + +European countries have days of week starting with monday (number 1), then tuesday (number 2) and till sunday (number 7). Write a function `getLocalDay(date)` that returns the "european" day of week for `date`. + +```js no-beautify +let date = new Date(2012, 0, 3); // 3 Jan 2012 +alert( getLocalDay(date) ); // tuesday, should show 2 +``` diff --git a/1-js/05-data-types/10-date/4-get-date-ago/_js.view/solution.js b/1-js/05-data-types/10-date/4-get-date-ago/_js.view/solution.js new file mode 100644 index 00000000..9cce0fb9 --- /dev/null +++ b/1-js/05-data-types/10-date/4-get-date-ago/_js.view/solution.js @@ -0,0 +1,6 @@ +function getDateAgo(date, days) { + let dateCopy = new Date(date); + + dateCopy.setDate(date.getDate() - days); + return dateCopy.getDate(); +} diff --git a/1-js/05-data-types/10-date/4-get-date-ago/_js.view/test.js b/1-js/05-data-types/10-date/4-get-date-ago/_js.view/test.js new file mode 100644 index 00000000..255acffe --- /dev/null +++ b/1-js/05-data-types/10-date/4-get-date-ago/_js.view/test.js @@ -0,0 +1,27 @@ +describe("getDateAgo", function() { + + it("1 day before 02.01.2015 -> day 1", function() { + assert.equal(getDateAgo(new Date(2015, 0, 2), 1), 1); + }); + + + it("2 days before 02.01.2015 -> day 31", function() { + assert.equal(getDateAgo(new Date(2015, 0, 2), 2), 31); + }); + + it("100 days before 02.01.2015 -> day 24", function() { + assert.equal(getDateAgo(new Date(2015, 0, 2), 100), 24); + }); + + it("365 days before 02.01.2015 -> day 2", function() { + assert.equal(getDateAgo(new Date(2015, 0, 2), 365), 2); + }); + + it("does not modify the given date", function() { + let date = new Date(2015, 0, 2); + let dateCopy = new Date(date); + getDateAgo(dateCopy, 100); + assert.equal(date.getTime(), dateCopy.getTime()); + }); + +}); diff --git a/1-js/05-data-types/10-date/4-get-date-ago/solution.md b/1-js/05-data-types/10-date/4-get-date-ago/solution.md new file mode 100644 index 00000000..7abc7ab9 --- /dev/null +++ b/1-js/05-data-types/10-date/4-get-date-ago/solution.md @@ -0,0 +1,27 @@ +The idea is simple: to substract given number of days from `date`: + +```js +function getDateAgo(date, days) { + date.setDate(date.getDate() - days); + return date.getDate(); +} +``` + +...But the function should not change `date`. That's an important thing, because the outer code which gives us the date does not expect it to change. + +To implement it let's clone the date, like this: + +```js run +function getDateAgo(date, days) { + let dateCopy = new Date(date); + + dateCopy.setDate(date.getDate() - days); + return dateCopy.getDate(); +} + +let date = new Date(2015, 0, 2); + +alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015) +alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014) +alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014) +``` diff --git a/1-js/05-data-types/10-date/4-get-date-ago/task.md b/1-js/05-data-types/10-date/4-get-date-ago/task.md new file mode 100644 index 00000000..40dcd926 --- /dev/null +++ b/1-js/05-data-types/10-date/4-get-date-ago/task.md @@ -0,0 +1,21 @@ +importance: 4 + +--- + +# Which day of month was many days ago? + +Create a function `getDateAgo(date, days)` to return the day of month `days` ago from the `date`. + +For instance, if today is 20th, then `getDateAgo(new Date(), 1)` should be 19th and `getDateAgo(new Date(), 2)` should be 18th. + +Should also work over months/years reliably: + +```js +let date = new Date(2015, 0, 2); + +alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015) +alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014) +alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014) +``` + +P.S. The function should not modify the given `date`. diff --git a/1-js/05-data-types/10-date/5-last-day-of-month/_js.view/solution.js b/1-js/05-data-types/10-date/5-last-day-of-month/_js.view/solution.js new file mode 100644 index 00000000..d0d7d37f --- /dev/null +++ b/1-js/05-data-types/10-date/5-last-day-of-month/_js.view/solution.js @@ -0,0 +1,4 @@ +function getLastDayOfMonth(year, month) { + let date = new Date(year, month + 1, 0); + return date.getDate(); +} diff --git a/1-js/05-data-types/10-date/5-last-day-of-month/_js.view/test.js b/1-js/05-data-types/10-date/5-last-day-of-month/_js.view/test.js new file mode 100644 index 00000000..4ff3e116 --- /dev/null +++ b/1-js/05-data-types/10-date/5-last-day-of-month/_js.view/test.js @@ -0,0 +1,13 @@ +describe("getLastDayOfMonth", function() { + it("last day of 01.01.2012 - 31", function() { + assert.equal(getLastDayOfMonth(2012, 0), 31); + }); + + it("last day of 01.02.2012 - 29 (leap year)", function() { + assert.equal(getLastDayOfMonth(2012, 1), 29); + }); + + it("last day of 01.02.2013 - 28", function() { + assert.equal(getLastDayOfMonth(2013, 1), 28); + }); +}); diff --git a/1-js/05-data-types/10-date/5-last-day-of-month/solution.md b/1-js/05-data-types/10-date/5-last-day-of-month/solution.md new file mode 100644 index 00000000..65f61c5b --- /dev/null +++ b/1-js/05-data-types/10-date/5-last-day-of-month/solution.md @@ -0,0 +1,13 @@ +Let's create a date using the next month, but pass zero as the day: +```js run +function getLastDayOfMonth(year, month) { + let date = new Date(year, month + 1, 0); + return date.getDate(); +} + +alert( getLastDayOfMonth(2012, 0) ); // 31 +alert( getLastDayOfMonth(2012, 1) ); // 29 +alert( getLastDayOfMonth(2013, 1) ); // 28 +``` + +Normally, dates start from 1, but technically we can pass any number, the date will autoadjust itself. So when we pass 0, then it means "one day before 1st day of the month", in other words: "the last day of the previous month". diff --git a/1-js/05-data-types/10-date/5-last-day-of-month/task.md b/1-js/05-data-types/10-date/5-last-day-of-month/task.md new file mode 100644 index 00000000..10dfb7a7 --- /dev/null +++ b/1-js/05-data-types/10-date/5-last-day-of-month/task.md @@ -0,0 +1,14 @@ +importance: 5 + +--- + +# Last day of month? + +Write a function `getLastDayOfMonth(year, month)` that returns the last day of month. Sometimes it is 30th, 31st or even 28/29th for Feb. + +Parameters: + +- `year` -- four-digits year, for instance 2012. +- `month` -- month, from 0 to 11. + +For instance, `getLastDayOfMonth(2012, 1) = 29` (leap year, Feb). diff --git a/1-js/05-data-types/10-date/6-get-seconds-today/solution.md b/1-js/05-data-types/10-date/6-get-seconds-today/solution.md new file mode 100644 index 00000000..91903d90 --- /dev/null +++ b/1-js/05-data-types/10-date/6-get-seconds-today/solution.md @@ -0,0 +1,26 @@ +To get the number of seconds, we can generate a date using the current day and time 00:00:00, then substract it from "now". + +The difference is the number of milliseconds from the beginning of the day, that we should divide by 1000 to get seconds: + +```js run +function getSecondsToday() { + let now = new Date(); + + // create an object using the current day/month/year + let today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + + let diff = now - today; // ms difference + return Math.round(diff / 1000); // make seconds +} + +alert( getSecondsToday() ); +``` + +An alternative solution would be to get hours/minutes/seconds and convert them to seconds: + +```js run +function getSecondsToday() { + let d = new Date(); + return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); +}; +``` diff --git a/1-js/05-data-types/10-date/6-get-seconds-today/task.md b/1-js/05-data-types/10-date/6-get-seconds-today/task.md new file mode 100644 index 00000000..3fbe1328 --- /dev/null +++ b/1-js/05-data-types/10-date/6-get-seconds-today/task.md @@ -0,0 +1,15 @@ +importance: 5 + +--- + +# How many seconds has passed today? + +Write a function `getSecondsToday()` that returns the number of seconds from the beginning of today. + +For instance, if now `10:00 am`, and there was no daylight savings shift, then: + +```js +getSecondsToday() == 36000 // (3600 * 10) +``` + +The function should work in any day. That is, it should not have a hard-coded value of "today". diff --git a/1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/solution.md b/1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/solution.md new file mode 100644 index 00000000..a89c8b3e --- /dev/null +++ b/1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/solution.md @@ -0,0 +1,15 @@ +To get the number of milliseconds till tomorrow, we can from "tomorrow 00:00:00" substract the current date. + +First, we generate that "tomorrow", and then do it: + +```js run +function getSecondsToTomorrow() { + let now = new Date(); + + // tomorrow date + let tomorrow = new Date(now.getFullYear(), now.getMonth(), *!*now.getDate()+1*/!*); + + let diff = tomorrow - now; // difference in ms + return Math.round(diff / 1000); // convert to seconds +} +``` diff --git a/1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/task.md b/1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/task.md new file mode 100644 index 00000000..e0590302 --- /dev/null +++ b/1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/task.md @@ -0,0 +1,15 @@ +importance: 5 + +--- + +# How many seconds till tomorrow? + +Create a function `getSecondsToTomorrow()` that returns the number of seconds till tomorrow. + +For instance, if now is `23:00`, then: + +```js +getSecondsToTomorrow() == 3600 +``` + +P.S. The function should work at any day, the "today" is not hardcoded. diff --git a/1-js/05-data-types/10-date/8-format-date-relative/_js.view/solution.js b/1-js/05-data-types/10-date/8-format-date-relative/_js.view/solution.js new file mode 100644 index 00000000..4695354a --- /dev/null +++ b/1-js/05-data-types/10-date/8-format-date-relative/_js.view/solution.js @@ -0,0 +1,33 @@ + +function formatDate(date) { + let diff = new Date() - date; // the difference in milliseconds + + if (diff < 1000) { // less than 1 second + return 'right now'; + } + + let sec = Math.floor(diff / 1000); // convert diff to seconds + + if (sec < 60) { + return sec + ' sec. ago'; + } + + let min = Math.floor(diff / 60000); // convert diff to minutes + if (min < 60) { + return min + ' min. ago'; + } + + // format the date + // add leading zeroes to single-digit day/month/hours/minutes + let d = date; + d = [ + '0' + d.getDate(), + '0' + (d.getMonth() + 1), + '' + d.getFullYear(), + '0' + d.getHours(), + '0' + d.getMinutes() + ].map(component => component.slice(-2)); // take last 2 digits of every component + + // join the components into date + return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':'); +} diff --git a/1-js/05-data-types/10-date/8-format-date-relative/_js.view/test.js b/1-js/05-data-types/10-date/8-format-date-relative/_js.view/test.js new file mode 100644 index 00000000..9b4cb2f5 --- /dev/null +++ b/1-js/05-data-types/10-date/8-format-date-relative/_js.view/test.js @@ -0,0 +1,18 @@ +describe("formatDate", function() { + it("shows 1ms ago as \"right now\"", function() { + assert.equal(formatDate(new Date(new Date - 1)), 'right now'); + }); + + it('"30 seconds ago"', function() { + assert.equal(formatDate(new Date(new Date - 30 * 1000)), "30 sec. ago"); + }); + + it('"5 minutes ago"', function() { + assert.equal(formatDate(new Date(new Date - 5 * 60 * 1000)), "5 min. ago"); + }); + + it("older dates as DD.MM.YY HH:mm", function() { + assert.equal(formatDate(new Date(2014, 2, 1, 11, 22, 33)), "01.03.14 11:22"); + }); + +}); diff --git a/1-js/05-data-types/10-date/8-format-date-relative/solution.md b/1-js/05-data-types/10-date/8-format-date-relative/solution.md new file mode 100644 index 00000000..87864be7 --- /dev/null +++ b/1-js/05-data-types/10-date/8-format-date-relative/solution.md @@ -0,0 +1,45 @@ +To get the time from `date` till now -- let's substract the dates. + +```js run +function formatDate(date) { + let diff = new Date() - date; // the difference in milliseconds + + if (diff < 1000) { // less than 1 second + return 'right now'; + } + + let sec = Math.floor(diff / 1000); // convert diff to seconds + + if (sec < 60) { + return sec + ' sec. ago'; + } + + let min = Math.floor(diff / 60000); // convert diff to minutes + if (min < 60) { + return min + ' min. ago'; + } + + // format the date + // add leading zeroes to single-digit day/month/hours/minutes + let d = date; + d = [ + '0' + d.getDate(), + '0' + (d.getMonth() + 1), + '' + d.getFullYear(), + '0' + d.getHours(), + '0' + d.getMinutes() + ].map(component => component.slice(-2)); // take last 2 digits of every component + + // join the components into date + return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':'); +} + +alert( formatDate(new Date(new Date - 1)) ); // "right now" + +alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago" + +alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago" + +// yesterday's date like 31.12.2016, 20:00 +alert( formatDate(new Date(new Date - 86400 * 1000)) ); +``` diff --git a/1-js/05-data-types/10-date/8-format-date-relative/task.md b/1-js/05-data-types/10-date/8-format-date-relative/task.md new file mode 100644 index 00000000..7b341ca2 --- /dev/null +++ b/1-js/05-data-types/10-date/8-format-date-relative/task.md @@ -0,0 +1,25 @@ +importance: 4 + +--- + +# Format the relative date + +Write a function `formatDate(date)` that should format `date` as follows: + +- If since `date` passed less than 1 second, then `"right now"`. +- Otherwise, if since `date` passed less than 1 minute, then `"n sec. ago"`. +- Otherwise, if less than an hour, then `"m min. ago"`. +- Otherwise, the full date in the format `"DD.MM.YY HH:mm"`. That is: `"day.month.year hours:minutes"`, all in 2-digit format, e.g. `31.12.16 10:00`. + +For instance: + +```js +alert( formatDate(new Date(new Date - 1)) ); // "right now" + +alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago" + +alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago" + +// yesterday's date like 31.12.2016, 20:00 +alert( formatDate(new Date(new Date - 86400 * 1000)) ); +``` diff --git a/1-js/05-data-types/10-date/article.md b/1-js/05-data-types/10-date/article.md new file mode 100644 index 00000000..ace3f932 --- /dev/null +++ b/1-js/05-data-types/10-date/article.md @@ -0,0 +1,423 @@ +# Date and time + +Let's meet a new built-in object: [Date](mdn:js/Date). It stores the date, time and provides methods for date/time management. + +For instance, we can use it to store creation/modification times, or to measure time, or just to print out the current date. + +[cut] + +## Creation + +To create a new `Date` object call `new Date()` with one of the following arguments: + +`new Date()` +: Without arguments -- create a `Date` object for the current date and time: + + ```js run + let now = new Date(); + alert( now ); // shows current date/time + ``` + +`new Date(milliseconds)` +: Create a `Date` object with the time equal to number of milliseconds (1/1000 of a second) passed after the Jan 1st of 1970 UTC+0. + + ```js run + // 0 means 01.01.1970 UTC+0 + let Jan01_1970 = new Date(0); + alert( Jan01_1970 ); + + // now add 24 hours, get 02.01.1970 UTC+0 + let Jan02_1970 = new Date(24 * 3600 * 1000); + alert( Jan02_1970 ); + ``` + + The number of milliseconds that has passed since the beginning of 1970 is called a *timestamp*. + + It's a lightweight numeric representation of a date. We can always create a date from a timestamp using `new Date(timestamp)` and convert the existing `Date` object to a timestamp using the `date.getTime()` method (see below). + +`new Date(datestring)` +: If there is a single argument, and it's a string, then it is parsed with the `Date.parse` algorithm (see below). + + + ```js run + let date = new Date("2017-01-26"); + alert(date); // Thu Jan 26 2017 ... + ``` + +`new Date(year, month, date, hours, minutes, seconds, ms)` +: Create the date with the given components in the local time zone. Only two first arguments are obligatory. + + Note: + + - The `year` must have 4 digits: `2013` is okay, `98` is not. + - The `month` count starts with `0` (Jan), up to `11` (Dec). + - The `date` parameter is actually the day of month, if absent then `1` is assumed. + - If `hours/minutes/seconds/ms` is absent, they are assumed to be equal `0`. + + For instance: + + ```js + new Date(2011, 0, 1, 0, 0, 0, 0); // // 1 Jan 2011, 00:00:00 + new Date(2011, 0, 1); // the same, hours etc are 0 by default + ``` + + The minimal precision is 1 ms (1/1000 sec): + + ```js run + let date = new Date(2011, 0, 1, 2, 3, 4, 567); + alert( date ); // 1.01.2011, 02:03:04.567 + ``` + +## Access date components + +There are many methods to access the year, month and so on from the `Date` object. But they can be easily remembered when categorized. + +`getFullYear()` +: Get the year (4 digits) + +`getMonth()` +: Get the month, **from 0 to 11**. + +`getDate()` +: Get the day of month, from 1 to 31, the name of the method does look a little bit strange. + +`getHours(), getMinutes(), getSeconds(), getMilliseconds()` +: Get the corresponding time components. + +```warn header="Not `getYear()`, but `getFullYear()`" +Many JavaScript engines implement a non-standard method `getYear()`. This method is deprecated. It returns 2-digit year sometimes. Please never use it. There is `getFullYear()` for the year. +``` + +Additionally, we can get a day of week: + +`getDay()` +: Get the day of week, from `0` (Sunday) to `6` (Saturday). The first day is always Sunday, in some countries that's not so, but can't be changed. + +**All the methods above return the components relative to the local time zone.** + +There are also their UTC-counterparts, that return day, month, year and so on for the time zone UTC+0: `getUTCFullYear()`, `getUTCMonth()`, `getUTCDay()`. Just insert the `"UTC"` right after `"get"`. + +If your local time zone is shifted relative to UTC, then the code below shows different hours: + +```js run +// currend date +let date = new Date(); + +// the hour in your current time zone +alert( date.getHours() ); + +// the hour in UTC+0 time zone (London time without daylight savings) +alert( date.getUTCHours() ); +``` + +Besides the given methods, there are two special ones, that do not have a UTC-variant: + +`getTime()` +: Returns the timestamp for the date -- a number of milliseconds passed from the January 1st of 1970 UTC+0. + +`getTimezoneOffset()` +: Returns the difference between the local time zone and UTC, in minutes: + + ```js run + // if you are in timezone UTC-1, outputs 60 + // if you are in timezone UTC+3, outputs -180 + alert( new Date().getTimezoneOffset() ); + + ``` + +## Setting date components + +The following methods allow to set date/time components: + +- `setFullYear(year [, month, date])` +- `setMonth(month [, date])` +- `setDate(date)` +- `setHours(hour [, min, sec, ms])` +- `setMinutes(min [, sec, ms])` +- `setSeconds(sec [, ms])` +- `setMilliseconds(ms)` +- `setTime(milliseconds)` (sets the whole date by milliseconds since 01.01.1970 UTC) + +Every one of them except `setTime()` has a UTC-variant, for instance: `setUTCHours()`. + +As we can see, some methods can set multiple components at once, for example `setHours`. The components that are not mentioned are not modified. + +For instance: + +```js run +let today = new Date(); + +today.setHours(0); +alert(today); // still today, but the hour is changed to 0 + +today.setHours(0, 0, 0, 0); +alert(today); // still today, now 00:00:00 sharp. +``` + +## Autocorrection + +The *autocorrection* is a very handy feature of `Date` objects. We can set out-of-range values, and it will auto-adjust itself. + +For instance: + +```js run +let date = new Date(2013, 0, *!*32*/!*); // 32 Jan 2013 ?!? +alert(date); // ...is 1st Feb 2013! +``` + +Out-of-range date components are distributed automatically. + +Let's say we need to increase the date "28 Feb 2016" by 2 days. It may be "2 Mar" or "1 Mar" in case of a leap-year. We don't need to think about it. Just add 2 days. The `Date` object will do the rest: + +```js run +let date = new Date(2016, 1, 28); +*!* +date.setDate(date.getDate() + 2); +*/!* + +alert( date ); // 1 Mar 2016 +``` + +That feature is often used to get the date after the given period of time. For instance, let's get the date for "70 seconds after now": + +```js run +let date = new Date(); +date.setSeconds(date.getSeconds() + 70); + +alert( date ); // shows the correct date +``` + +We can also set zero or even negative values. For example: + +```js run +let date = new Date(2016, 0, 2); // 2 Jan 2016 + +date.setDate(1); // set day 1 of month +alert( date ); + +date.setDate(0); // min day is 1, so the last day of the previous month is assumed +alert( date ); // 31 Dec 2015 +``` + +## Date to number, date diff + +When a `Date` object is converted to number, it becomes the timestamp same as `date.getTime()`: + +```js run +let date = new Date(); +alert(+date); // the number of milliseconds, same as date.getTime() +``` + +The important side effect: dates can be substracted, the result is their difference in ms. + +That can be used for time measurements: + +```js run +let start = new Date(); // start counting + +// do the job +for (let i = 0; i < 100000; i++) { + let doSomething = i * i * i; +} + +let end = new Date(); // done + +alert( `The loop took ${end - start} ms` ); +``` + +## Date.now() + +If we only want to measure the difference, we don't need the `Date` object. + +There's a special method `Date.now()` that returns the current timestamp. + +It is semantically equivalent to `new Date().getTime()`, but it doesn't create an intermediate `Date` object. So it's faster and doesn't put pressure on garbage collection. + +It is used mostly for convenience or when performance matters, like in games in JavaScript or other specialized applications. + +So this is probably better: + +```js run +*!* +let start = Date.now(); // milliseconds count from 1 Jan 1970 +*/!* + +// do the job +for (let i = 0; i < 100000; i++) { + let doSomething = i * i * i; +} + +*!* +let end = Date.now(); // done +*/!* + +alert( `The loop took ${end - start} ms` ); // substract numbers, not dates +``` + +## Benchmarking + +If we want a reliable benchmark of CPU-hungry function, we should be careful. + +For instance, let's measure two functions that calculate the difference between two dates: which one is faster? + +```js +// we have date1 and date2, which function faster returns their difference in ms? +function diffSubstract(date1, date2) { + return date2 - date1; +} + +// or +function diffGetTime(date1, date2) { + return date2.getTime() - date1.getTime(); +} +``` + +These two do exactly the same thing, but one of them uses an explicit `date.getTime()` to get the date in ms, and the other one relies on a date-to-number transform. Their result is always the same. + +So, which one is faster? + +The first idea may be to run them many times in a row and measure the time difference. For our case, functions are very simple, so we have to do it around 100000 times. + +Let's measure: + +```js run +function diffSubstract(date1, date2) { + return date2 - date1; +} + +function diffGetTime(date1, date2) { + return date2.getTime() - date1.getTime(); +} + +function bench(f) { + let date1 = new Date(0); + let date2 = new Date(); + + let start = Date.now(); + for (let i = 0; i < 100000; i++) f(date1, date2); + return Date.now() - start; +} + +alert( 'Time of diffSubstract: ' + bench(diffSubstract) + 'ms' ); +alert( 'Time of diffGetTime: ' + bench(diffGetTime) + 'ms' ); +``` + +Wow! Using `getTime()` is so much faster! That's because there's no type conversion, it is much easier for engines to optimize. + +Okay, we have something. But that's not a good benchmark yet. + +Imagine that at the time of running `bench(diffSubstract)` CPU was doing something in parallel, and it was taking resources. And by the time of running `bench(diffGetTime)` the work has finished. + +A pretty real scenario for a modern multi-process OS. + +As a result, the first benchmark will have less CPU resources than the second. That may lead to wrong results. + +**For more reliable benchmarking, the whole pack of benchmarks should be rerun multiple times.** + +Here's the code example: + +```js run +function diffSubstract(date1, date2) { + return date2 - date1; +} + +function diffGetTime(date1, date2) { + return date2.getTime() - date1.getTime(); +} + +function bench(f) { + let date1 = new Date(0); + let date2 = new Date(); + + let start = Date.now(); + for (let i = 0; i < 100000; i++) f(date1, date2); + return Date.now() - start; +} + +let time1 = 0; +let time2 = 0; + +*!* +// run bench(upperSlice) and bench(upperLoop) each 10 times alternating +for (let i = 0; i < 10; i++) { + time1 += bench(diffSubstract); + time2 += bench(diffGetTime); +} +*/!* + +alert( 'Total time for diffSubstract: ' + time1 ); +alert( 'Total time for diffGetTime: ' + time2 ); +``` + +Modern JavaScript engines start applying advanced optimizations only to "hot code" that executes many times (no need to optimize rarely executed things). So, in the example above, first executions are not well-optimized. We may want to add a heat-up run: + +```js +// added for "heating up" prior to the main loop +bench(diffSubstract); +bench(diffGetTime); + +// now benchmark +for (let i = 0; i < 10; i++) { + time1 += bench(diffSubstract); + time2 += bench(diffGetTime); +} +``` + +```warn header="Be careful doing microbenchmarking" +Modern JavaScript engines perform many optimizations. They may tweak results of "artificial tests" compared to "normal usage", especially when we benchmark something very small. So if you seriously want to understand performance, then please study how the JavaScript engine works. And then you probably won't need microbenchmarks at all. + +The great pack of articles about V8 can be found at . +``` + +## Date.parse from a string + +The method [Date.parse(str)](mdn:js/Date/parse) can read a date from a string. + +The string format should be: `YYYY-MM-DDTHH:mm:ss.sssZ`, where: + +- `YYYY-MM-DD` -- is the date: year-month-day. +- The character `"T"` is used as the delimiter. +- `HH:mm:ss.sss` -- is the time: hours, minutes, seconds and milliseconds. +- The optional `'Z'` part denotes the time zone in the format `+-hh:mm`. A single letter `Z` that would mean UTC+0. + +Shorter variants are also possible, like `YYYY-MM-DD` or `YYYY-MM` or even `YYYY`. + +The call to `Date.parse(str)` parses the string in the given format and returns the timestamp (number of milliseconds from 1 Jan 1970 UTC+0). If the format is invalid, returns `NaN`. + +For instance: + +```js run +let ms = Date.parse('2012-01-26T13:51:50.417-07:00'); + +alert(ms); // 1327611110417 (timestamp) +``` + +We can instantly create a `new Date` object from the timestamp: + +```js run +let date = new Date( Date.parse('2012-01-26T13:51:50.417-07:00') ); + +alert(date); +``` + +## Summary + +- Date and time in JavaScript are represented with the [Date](mdn:js/Date) object. We can't create "only date" or "only time": `Date` objects always carry both. +- Months are counted from zero (yes, January is a zero month). +- Days of week in `getDay()` are also counted from zero (that's Sunday). +- `Date` auto-corrects itself when out-of-range components are set. Good for adding/substracting days/months/hours. +- Dates can be substracted, giving their difference in milliseconds. That's because a `Date` becomes the timestamp if converted to a number. +- Use `Date.now()` to get the current timestamp fast. + +Note that unlike many other systems, timestamps in JavaScript are in milliseconds, not in seconds. + +Also, sometimes we need more precise time measurements. JavaScript itself does not have a way to measure time in microseconds (1 millionth of a second), but most environments provide it. For instance, browser has [performance.now()](mdn:api/Performance/now) that gives the number of milliseconds from the start of page loading with microsecond precision (3 digits after the point): + +```js run +alert(`Loading started ${performance.now()}ms ago`); +// Something like: "Loading started 34731.26000000001ms ago" +// .26 is microseconds (260 microseconds) +// more than 3 digits after the decimal point are precision errors, but only 3 first are correct +``` + +Node.JS has `microtime` module and other ways. Technically, any device and environment allows to get more precision, it's just not in `Date`. diff --git a/1-js/05-data-types/11-json/1-serialize-object/solution.md b/1-js/05-data-types/11-json/1-serialize-object/solution.md new file mode 100644 index 00000000..db1bcaa8 --- /dev/null +++ b/1-js/05-data-types/11-json/1-serialize-object/solution.md @@ -0,0 +1,13 @@ + + +```js +let user = { + name: "John Smith", + age: 35 +}; + +*!* +let user2 = JSON.parse(JSON.stringify(user)); +*/!* +``` + diff --git a/1-js/05-data-types/11-json/1-serialize-object/task.md b/1-js/05-data-types/11-json/1-serialize-object/task.md new file mode 100644 index 00000000..cbb04609 --- /dev/null +++ b/1-js/05-data-types/11-json/1-serialize-object/task.md @@ -0,0 +1,14 @@ +importance: 5 + +--- + +# Turn the object into JSON and back + +Turn the `leader` into JSON and then read it back into another variable. + +```js +let user = { + name: "John Smith", + age: 35 +}; +``` diff --git a/1-js/05-data-types/11-json/2-serialize-event-circular/solution.md b/1-js/05-data-types/11-json/2-serialize-event-circular/solution.md new file mode 100644 index 00000000..7a3a533b --- /dev/null +++ b/1-js/05-data-types/11-json/2-serialize-event-circular/solution.md @@ -0,0 +1,30 @@ + +```js run +let room = { + number: 23 +}; + +let meetup = { + title: "Conference", + occupiedBy: [{name: "John"}, {name: "Alice"}], + place: room +}; + +room.occupiedBy = meetup; +meetup.self = meetup; + +alert( JSON.stringify(meetup, function replacer(key, value) { + return (key != "" && value == meetup) ? undefined : value; +})); + +/* +{ + "title":"Conference", + "occupiedBy":[{"name":"John"},{"name":"Alice"}], + "place":{"number":23} +} +*/ +``` + +Here we also need to test `key==""` to exclude the first call where it is normal that `value` is `meetup`. + diff --git a/1-js/05-data-types/11-json/2-serialize-event-circular/task.md b/1-js/05-data-types/11-json/2-serialize-event-circular/task.md new file mode 100644 index 00000000..8b3963dd --- /dev/null +++ b/1-js/05-data-types/11-json/2-serialize-event-circular/task.md @@ -0,0 +1,42 @@ +importance: 5 + +--- + +# Exclude backreferences + +In simple cases of circular references, we can exclude an offending property from serialization by its name. + +But sometimes there are many backreferences. And names may be used both in circular references and normal properties. + +Write `replacer` function to stringify everything, but remove properties that reference `meetup`: + +```js run +let room = { + number: 23 +}; + +let meetup = { + title: "Conference", + occupiedBy: [{name: "John"}, {name: "Alice"}], + place: room +}; + +*!* +// circular references +room.occupiedBy = meetup; +meetup.self = meetup; +*/!* + +alert( JSON.stringify(meetup, function replacer(key, value) { + /* your code */ +})); + +/* result should be: +{ + "title":"Conference", + "occupiedBy":[{"name":"John"},{"name":"Alice"}], + "place":{"number":23} +} +*/ +``` + diff --git a/1-js/05-data-types/11-json/article.md b/1-js/05-data-types/11-json/article.md new file mode 100644 index 00000000..096570e3 --- /dev/null +++ b/1-js/05-data-types/11-json/article.md @@ -0,0 +1,527 @@ +# JSON methods, toJSON + +Let's say we have a complex object, and we'd like to convert it into a string, to send it over a network, or just to output it for logging purposes. + +Naturally, such a string should include all important properties. + +We could implement the conversion like this: + +```js run +let user = { + name: "John", + age: 30, + +*!* + toString() { + return `{name: "${this.name}", age: ${this.age}}`; + } +*/!* +}; + +alert(user); // {name: "John", age: 30} +``` + +...But in the process of development, new properties are added, old properties are renamed and removed. Updating such `toString` every time can become a pain. We could try to loop over properties in it, but what if the object is complex and has nested objects in properties? We'd need to implement their conversion as well. And, if we're sending the object over a network, then we also need to supply the code to "read" our object on the receiving side. + +Luckily, there's no need to write the code to handle all this. The task has been solved already. + +[cut] + +## JSON.stringify + +The [JSON](http://en.wikipedia.org/wiki/JSON) (JavaScript Object Notation) is a general format to represent values and objects. It is described as in [RFC 4627](http://tools.ietf.org/html/rfc4627) standard. Initially it was made for JavaScript, but many other languages have libraries to handle it as well. So it's easy to use JSON for data exchange when the client uses JavaScript and the server is written on Ruby/PHP/Java/Whatever. + +JavaScript provides methods: + +- `JSON.stringify` to convert objects into JSON. +- `JSON.parse` to convert JSON back into an object. + +For instance, here's we `JSON.stringify` a student: +```js run +let student = { + name: 'John', + age: 30, + isAdmin: false, + courses: ['html', 'css', 'js'], + wife: null +}; + +*!* +let json = JSON.stringify(student); +*/!* + +alert(typeof json); // we've got a string! + +alert(json); +*!* +/* JSON-encoded object: +{ + "name": "John", + "age": 30, + "isAdmin": false, + "courses": ["html", "css", "js"], + "wife": null +} +*/ +*/!* +``` + +The method `JSON.stringify(student)` takes the object and converts it into a string. + +The resulting `json` string is a called *JSON-encoded* or *serialized* or *stringified* or *marshalled* object. We are ready to send it over the wire or put into plain data store. + + +Please note that JSON-encoded object has several important differences from the object literal: + +- Strings use double quotes. No single quotes or backticks in JSON. So `'John'` becomes `"John"`. +- Object property names are double-quoted also. That's obligatory. So `age:30` becomes `"age":30`. + +`JSON.stringify` can be applied to primitives as well. + +Natively supported JSON types are: + +- Objects `{ ... }` +- Arrays `[ ... ]` +- Primitives: + - strings, + - numbers, + - boolean values `true/false`, + - `null`. + +For instance: + +```js run +// a number in JSON is just a number +alert( JSON.stringify(1) ) // 1 + +// a string in JSON is still a string, but double-quoted +alert( JSON.stringify('test') ) // "test" + +alert( JSON.stringify(true) ); // true + +alert( JSON.stringify([1, 2, 3]) ); // [1,2,3] +``` + +JSON is data-only cross-language specification, so some JavaScript-specific object properties are skipped by `JSON.stringify`. + +Namely: + +- Function properties (methods). +- Symbolic properties. +- Properties that store `undefined`. + +```js run +let user = { + sayHi() { // ignored + alert("Hello"); + }, + [Symbol("id")]: 123, // ignored + something: undefined // ignored +}; + +alert( JSON.stringify(user) ); // {} (empty object) +``` + +Usually that's fine. If that's not what we want, then soon we'll see how to customize the process. + +The great thing is that nested objects are supported and converted automatically. + +For instance: + +```js run +let meetup = { + title: "Conference", +*!* + room: { + number: 123, + participants: ["john", "ann"] + } +*/!* +}; + +alert( JSON.stringify(meetup) ); +/* The whole structure is stringified: +{ + "title":"Conference", + "room":{"number":23,"participants":["john","ann"]}, +} +*/ +``` + +The important limitation: there must be no circular references. + +For instance: + +```js run +let room = { + number: 23 +}; + +let meetup = { + title: "Conference", + participants: ["john", "ann"] +}; + +meetup.room = room; // meetup references room +room.occupiedBy = meetup; // room references meetup + +*!* +JSON.stringify(meetup); // Error: Converting circular structure to JSON +*/!* +``` + +Here, the conversion fails, because of circular reference: `room.occupiedBy` references `meetup`, and `meetup.place` references `room`: + +![](json-meetup.png) + + +## Excluding and transforming: replacer + +The full syntax of `JSON.stringify` is: + +```js +let json = JSON.stringify(value[, replacer, space]) +``` + +value +: A value to encode. + +replacer +: Array of properties to encode or a mapping function `function(key, value)`. + +space +: Amount of space to use for formatting + +Most of time, `JSON.stringify` is used with first argument only. But if we need to fine-tune the replacement process, like to filter out circular references, we can use the second argument of `JSON.stringify`. + +If we pass an array of properties to it, only these properties will be encoded. + +For instance: + +```js run +let room = { + number: 23 +}; + +let meetup = { + title: "Conference", + participants: [{name: "John"}, {name: "Alice"}], + place: room // meetup references room +}; + +room.occupiedBy = meetup; // room references meetup + +alert( JSON.stringify(meetup, *!*['title', 'participants']*/!*) ); +// {"title":"Conference","participants":[{},{}]} +``` + +Here we are probably too strict. The property list is applied to the whole object structure. So participants are empty, because `name` is not in the list. + +Let's include every property except `room.occupiedBy` that would cause the circular reference: + +```js run +let room = { + number: 23 +}; + +let meetup = { + title: "Conference", + participants: [{name: "John"}, {name: "Alice"}], + place: room // meetup references room +}; + +room.occupiedBy = meetup; // room references meetup + +alert( JSON.stringify(meetup, *!*['title', 'participants', 'place', 'name', 'number']*/!*) ); +/* +{ + "title":"Conference", + "participants":[{"name":"John"},{"name":"Alice"}], + "place":{"number":23} +} +*/ +``` + +Now everything except `occupiedBy` is serialized. But the list of properties is quite long. + +Fortunately, we can use a function instead of an array as the `replacer`. + +The function will be called for every `(key,value)` pair and should return the "replaced" value, which will be used instead of the original one. + +In our case, we can return `value` "as is" for everything except `occupiedBy`. To ignore `occupiedBy`, the code below returns `undefined`: + +```js run +let room = { + number: 23 +}; + +let meetup = { + title: "Conference", + participants: [{name: "John"}, {name: "Alice"}], + place: room // meetup references room +}; + +alert( JSON.stringify(meetup, function replacer(key, value) { + alert(`${key}: ${value}`); // to see what replacer gets + return (key == 'occupiedBy') ? undefined : value; +})); + +/* key:value pairs that come to replacer: +: [object Object] +title: Conference +participants: [object Object],[object Object] +0: [object Object] +name: John +1: [object Object] +name: Alice +place: [object Object] +number: 23 +*/ +``` + +Please note that `replacer` function gets every key/value pair including nested objects and array items. It is applied recursively. The value of `this` inside `replacer` is the object that contains the current property. + +The first call is special. It is made using a special "wrapper object": `{"": meetup}`. In other words, the first `(key,value)` pair has an empty key, and the value is the target object as a whole. That's why the first line is `":[object Object]"` in the example above. + +The idea is to provide as much power for `replacer` as possible: it has a chance to analyze and replace/skip the whole object if necessary. + + +## Formatting: spacer + +The third argument of `JSON.stringify(value, replacer, spaces)` is the number of spaces to use for pretty formatting. + +Previously, all stringified objects had no indents and extra spaces. That's fine if we want to send an object over a network. The `spacer` argument is used exclusively for a nice output. + +Here `spacer = 2` tells JavaScript to show nested objects on multiple lines, with indentation of 2 spaces inside an object: + +```js run +let user = { + name: "John", + age: 25, + roles: { + isAdmin: false, + isEditor: true + } +}; + +alert(JSON.stringify(user, null, 2)); +/* two-space indents: +{ + "name": "John", + "age": 25, + "roles": { + "isAdmin": false, + "isEditor": true + } +} +*/ + +/* for JSON.stringify(user, null, 4) the result would be more indented: +{ + "name": "John", + "age": 25, + "roles": { + "isAdmin": false, + "isEditor": true + } +} +*/ +``` + +The `spaces` parameter is used solely for logging and nice-output purposes. + +## Custom "toJSON" + +Like `toString` for a string conversion, an object may provide method `toJSON` for to-JSON conversion. `JSON.stringify` automatically calls it if available. + +For instance: + +```js run +let room = { + number: 23 +}; + +let meetup = { + title: "Conference", + date: new Date(Date.UTC(2017, 0, 1)), + room +}; + +alert( JSON.stringify(meetup) ); +/* + { + "title":"Conference", +*!* + "date":"2017-01-01T00:00:00.000Z", // (1) +*/!* + "room": {"number":23} // (2) + } +*/ +``` + +Here we can see that `date` `(1)` became a string. That's because all dates have a built-in `toJSON` method which returns such kind of string. + +Now let's add a custom `toJSON` for our object `room`: + +```js run +let room = { + number: 23, +*!* + toJSON() { + return this.number; + } +*/!* +}; + +let meetup = { + title: "Conference", + room +}; + +*!* +alert( JSON.stringify(room) ); // 23 +*/!* + +alert( JSON.stringify(meetup) ); +/* + { + "title":"Conference", +*!* + "room": 23 +*/!* + } +*/ +``` + +As we can see, `toJSON` is used both for the direct call `JSON.stringify(room)` and for the nested object. + + +## JSON.parse + +To decode a JSON-string, we need another method named [JSON.parse](mdn:js/JSON/parse). + +The syntax: +```js +let value = JSON.parse(str[, reviver]); +``` + +str +: JSON-string to parse. + +reviver +: Optional function(key,value) that will be called for each `(key,value)` pair and can transform the value. + +For instance: + +```js run +// stringified array +let numbers = "[0, 1, 2, 3]"; + +numbers = JSON.parse(numbers); + +alert( numbers[1] ); // 1 +``` + +Or for nested objects: + +```js run +let user = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }'; + +user = JSON.parse(user); + +alert( user.friends[1] ); // 1 +``` + +The JSON may be as complex as necessary, objects and arrays can include other objects and arrays. But they must obey the format. + +Here are typical mistakes in hand-written JSON (sometimes we have to write it for debugging purposes): + +```js +let json = `{ + *!*name*/!*: "John", // mistake: property name without quotes + "surname": *!*'Smith'*/!*, // mistake: single quotes in value (must be double) + *!*'isAdmin'*/!*: false // mistake: single quotes in key (must be double) + "birthday": *!*new Date(2000, 2, 3)*/!*, // mistake: no "new" is allowed, only bare values + "friends": [0,1,2,3] // here all fine +}`; +``` + +Besides, JSON does not support comments. Adding a comment to JSON makes it invalid. + +There's another format named [JSON5](http://json5.org/), which allows unquoted keys, comments etc. But this is a standalone library, not in the specification of the language. + +The regular JSON is that strict not because its developers are lazy, but to allow easy, reliable and very fast implementations of the parsing algorithm. + +## Using reviver + +Imagine, we got a stringified `meetup` object from the server. + +It looks like this: + +```js +// title: (meetup title), date: (meetup date) +let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; +``` + +...And now we need to *deserialize* it, to turn back into JavaScript object. + +Let's do it by calling `JSON.parse`: + +```js run +let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; + +let meetup = JSON.parse(str); + +*!* +alert( meetup.date.getDate() ); // Error! +*/!* +``` + +Whoops! An error! + +The value of `meetup.date` is a string, not a `Date` object. How could `JSON.parse` know that it should transform that string into a `Date`? + +Let's pass to `JSON.parse` the reviving function that returns all values "as is", but `date` will become a `Date`: + +```js run +let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; + +*!* +let meetup = JSON.parse(str, function(key, value) { + if (key == 'date') return new Date(value); + return value; +}); +*/!* + +alert( meetup.date.getDate() ); // now works! +``` + +By the way, that works for nested objects as well: + +```js run +let schedule = `{ + "meetups": [ + {"title":"Conference","date":"2017-11-30T12:00:00.000Z"}, + {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"} + ] +}`; + +schedule = JSON.parse(schedule, function(key, value) { + if (key == 'date') return new Date(value); + return value; +}); + +*!* +alert( schedule.meetups[1].date.getDate() ); // works! +*/!* +``` + + + +## Summary + +- JSON is a data format that has its own independent standard and libraries for most programming languages. +- JSON supports plain objects, arrays, strings, numbers, booleans and `null`. +- JavaScript provides methods [JSON.stringify](mdn:js/JSON/stringify) to serialize into JSON and [JSON.parse](mdn:js/JSON/parse) to read from JSON. +- Both methods support transformer functions for smart reading/writing. +- If an object has `toJSON`, then it is called by `JSON.stringify`. diff --git a/1-js/05-data-types/11-json/json-meetup.png b/1-js/05-data-types/11-json/json-meetup.png new file mode 100644 index 00000000..0a26e0a6 Binary files /dev/null and b/1-js/05-data-types/11-json/json-meetup.png differ diff --git a/1-js/05-data-types/11-json/json-meetup@2x.png b/1-js/05-data-types/11-json/json-meetup@2x.png new file mode 100644 index 00000000..b5f6a401 Binary files /dev/null and b/1-js/05-data-types/11-json/json-meetup@2x.png differ diff --git a/1-js/05-data-types/index.md b/1-js/05-data-types/index.md new file mode 100644 index 00000000..246e2bc9 --- /dev/null +++ b/1-js/05-data-types/index.md @@ -0,0 +1,3 @@ +# Data types + +More data structures and more in-depth study of the types. diff --git a/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md b/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md new file mode 100644 index 00000000..237b9ef9 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md @@ -0,0 +1,40 @@ +The solution using a loop: + +```js run +function sumTo(n) { + let sum = 0; + for (let i = 1; i <= n; i++) { + sum += i; + } + return sum; +} + +alert( sumTo(100) ); +``` + +The solution using recursion: + +```js run +function sumTo(n) { + if (n == 1) return 1; + return n + sumTo(n - 1); +} + +alert( sumTo(100) ); +``` + +The solution using the formula: `sumTo(n) = n*(n+1)/2`: + +```js run +function sumTo(n) { + return n * (n + 1) / 2; +} + +alert( sumTo(100) ); +``` + +P.S. Naturally, the formula is the fastest solution. It uses only 3 operations for any number `n`. The math helps! + +The loop variant is the second in terms of speed. In both the recursive and the loop variant we sum the same numbers. But the recursion involves nested calls and execution stack management. That also takes resources, so it's slower. + +P.P.S. The standard describes a "tail call" optimization: if the recursive call is the very last one in the function (like in `sumTo` above), then the outer function will not need to resume the execution and we don't need to remember its execution context. In that case `sumTo(100000)` is countable. But if your JavaScript engine does not support it, there will be an error: maximum stack size exceeded, because there's usually a limitation on the total stack size. diff --git a/1-js/06-advanced-functions/01-recursion/01-sum-to/task.md b/1-js/06-advanced-functions/01-recursion/01-sum-to/task.md new file mode 100644 index 00000000..cabc1329 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/01-sum-to/task.md @@ -0,0 +1,36 @@ +importance: 5 + +--- + +# Sum all numbers till the given one + +Write a function `sumTo(n)` that calculates the sum of numbers `1 + 2 + ... + n`. + +For instance: + +```js no-beautify +sumTo(1) = 1 +sumTo(2) = 2 + 1 = 3 +sumTo(3) = 3 + 2 + 1 = 6 +sumTo(4) = 4 + 3 + 2 + 1 = 10 +... +sumTo(100) = 100 + 99 + ... + 2 + 1 = 5050 +``` + +Make 3 solution variants: + +1. Using a for loop. +2. Using a recursion, cause `sumTo(n) = n + sumTo(n-1)` for `n > 1`. +3. Using the [arithmetic progression](https://en.wikipedia.org/wiki/Arithmetic_progression) formula. + +An example of the result: + +```js +function sumTo(n) { /*... your code ... */ } + +alert( sumTo(100) ); // 5050 +``` + +P.S. Which solution variant is the fastest? The slowest? Why? + +P.P.S. Can we use recursion to count `sumTo(100000)`? diff --git a/1-js/06-advanced-functions/01-recursion/02-factorial/solution.md b/1-js/06-advanced-functions/01-recursion/02-factorial/solution.md new file mode 100644 index 00000000..59040a2b --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/02-factorial/solution.md @@ -0,0 +1,21 @@ +By definition, a factorial is `n!` can be written as `n * (n-1)!`. + +In other words, the result of `factorial(n)` can be calculated as `n` multiplied by the result of `factorial(n-1)`. And the call for `n-1` can recursively descend lower, and lower, till `1`. + +```js run +function factorial(n) { + return (n != 1) ? n * factorial(n - 1) : 1; +} + +alert( factorial(5) ); // 120 +``` + +The basis of recursion is the value `1`. We can also make `0` the basis here, doesn't matter much, but gives one more recursive step: + +```js run +function factorial(n) { + return n ? n * factorial(n - 1) : 1; +} + +alert( factorial(5) ); // 120 +``` diff --git a/1-js/06-advanced-functions/01-recursion/02-factorial/task.md b/1-js/06-advanced-functions/01-recursion/02-factorial/task.md new file mode 100644 index 00000000..d2aef2d9 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/02-factorial/task.md @@ -0,0 +1,31 @@ +importance: 4 + +--- + +# Calculate factorial + +The [factorial](https://en.wikipedia.org/wiki/Factorial) of a natural number is a number multiplied by `"number minus one"`, then by `"number minus two"`, and so on till `1`. The factorial of `n` is denoted as `n!` + +We can write a definition of factorial like this: + +```js +n! = n * (n - 1) * (n - 2) * ...*1 +``` + +Values of factorials for different `n`: + +```js +1! = 1 +2! = 2 * 1 = 2 +3! = 3 * 2 * 1 = 6 +4! = 4 * 3 * 2 * 1 = 24 +5! = 5 * 4 * 3 * 2 * 1 = 120 +``` + +The task is to write a function `factorial(n)` that calculates `n!` using recursive calls. + +```js +alert( factorial(5) ); // 120 +``` + +P.S. Hint: `n!` can be written as `n * (n-1)!` For instance: `3! = 3*2! = 3*2*1! = 6` diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.png b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.png new file mode 100644 index 00000000..c45418ff Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.png differ diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree@2x.png b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree@2x.png new file mode 100644 index 00000000..6fc39ae1 Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree@2x.png differ diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/solution.md b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/solution.md new file mode 100644 index 00000000..91bcecc0 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/solution.md @@ -0,0 +1,110 @@ +The first solution we could try here is the recursive one. + +Fibonacci numbers are recursive by definition: + +```js run +function fib(n) { + return n <= 1 ? n : fib(n - 1) + fib(n - 2); +} + +alert( fib(3) ); // 2 +alert( fib(7) ); // 13 +// fib(77); // will be extremely slow! +``` + +...But for big values of `n` it's very slow. For instance, `fib(77)` may hang up the engine for some time eating all CPU resources. + +That's because the function makes too many subcalls. The same values are re-evaluated again and again. + +For instance, let's see a piece of calculations for `fib(5)`: + +```js no-beautify +... +fib(5) = fib(4) + fib(3) +fib(4) = fib(3) + fib(2) +... +``` + +Here we can see that the value of `fib(3)` is needed for both `fib(5)` and `fib(4)`. So `fib(3)` will be called and evaluated two times completely independently. + +Here's the full recursion tree: + +![fibonacci recursion tree](fibonacci-recursion-tree.png) + +We can clearly notice that `fib(3)` is evaluated two times and `fib(2)` is evaluated three times. The total amount of computations grows much faster than `n`, making it enormous even for `n=77`. + +We can optimize that by remembering already-evaluated values: if a value of say `fib(3)` is calculated once, then we can just reuse it in future computations. + +Another variant would be to give up recursion and use a totally different loop-based algorithm. + +Instead of going from `n` down to lower values, we can make a loop that starts from `1` and `2`, then gets `fib(3)` as their sum, then `fib(4)` as the sum of two previous values, then `fib(5)` and goes up and up, till it gets to the needed value. On each step we only need to remember two previous values. + +Here are the steps of the new algorithm in details. + +The start: + +```js +// a = fib(1), b = fib(2), these values are by definition 1 +let a = 1, b = 1; + +// get c = fib(3) as their sum +let c = a + b; + +/* we now have fib(1), fib(2), fib(3) +a b c +1, 1, 2 +*/ +``` + +Now we want to get `fib(4) = fib(2) + fib(3)`. + +Let's shift the variables: `a,b` will get `fib(2),fib(3)`, and `c` will get their sum: + +```js no-beautify +a = b; // now a = fib(2) +b = c; // now b = fib(3) +c = a + b; // c = fib(4) + +/* now we have the sequence: + a b c +1, 1, 2, 3 +*/ +``` + +The next step gives another sequence number: + +```js no-beautify +a = b; // now a = fib(3) +b = c; // now b = fib(4) +c = a + b; // c = fib(5) + +/* now the sequence is (one more number): + a b c +1, 1, 2, 3, 5 +*/ +``` + +...And so on until we get the needed value. That's much faster than recursion and involves no duplicate computations. + +The full code: + +```js run +function fib(n) { + let a = 1; + let b = 1; + for (let i = 3; i <= n; i++) { + let c = a + b; + a = b; + b = c; + } + return b; +} + +alert( fib(3) ); // 2 +alert( fib(7) ); // 13 +alert( fib(77) ); // 5527939700884757 +``` + +The loop starts with `i=3`, because the first and the second sequence values are hard-coded into variables `a=1`, `b=1`. + +The approach is called [dynamic programming bottom-up](https://en.wikipedia.org/wiki/Dynamic_programming). diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/task.md b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/task.md new file mode 100644 index 00000000..3cdadd21 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/task.md @@ -0,0 +1,25 @@ +importance: 5 + +--- + +# Fibonacci numbers + +The sequence of [Fibonacci numbers](https://en.wikipedia.org/wiki/Fibonacci_number) has the formula Fn = Fn-1 + Fn-2. In other words, the next number is a sum of the two preceding ones. + +First two numbers are `1`, then `2(1+1)`, then `3(1+2)`, `5(2+3)` and so on: `1, 1, 2, 3, 5, 8, 13, 21...`. + +Fibonacci numbers are related to the [Golden ratio](https://en.wikipedia.org/wiki/Golden_ratio) and many natural phenomena around us. + +Write a function `fib(n)` that returns the `n-th` Fibonacci number. + +An example of work: + +```js +function fib(n) { /* your code */ } + +alert(fib(3)); // 2 +alert(fib(7)); // 13 +alert(fib(77)); // 5527939700884757 +``` + +P.S. The function should be fast. The call to `fib(77)` should take no more than a fraction of a second. diff --git a/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/solution.md b/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/solution.md new file mode 100644 index 00000000..4e9de146 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/solution.md @@ -0,0 +1,88 @@ +# Loop-based solution + +The loop-based variant of the solution: + +```js run +let list = { + value: 1, + next: { + value: 2, + next: { + value: 3, + next: { + value: 4, + next: null + } + } + } +}; + +function printList(list) { + let tmp = list; + + while (tmp) { + alert(tmp.value); + tmp = tmp.next; + } + +} + +printList(list); +``` + +Please note that we use a temporary variable `tmp` to walk over the list. Technically, we could use a function parameter `list` instead: + +```js +function printList(list) { + + while(*!*list*/!*) { + alert(list.value); + list = list.next; + } + +} +``` + +...But that would be unwise. In the future we may need to extend a function, do something else with the list. If we change `list`, then we loose such ability. + +Talking about good variable names, `list` here is the list itself. The first element of it. And it should remain like that. That's clear and reliable. + +From the other side, the role of `tmp` is exclusively a list traversal, like `i` in the `for` loop. + +# Recursive solution + +The recursive variant of `printList(list)` follows a simple logic: to output a list we should output the current element `list`, then do the same for `list.next`: + +```js run +let list = { + value: 1, + next: { + value: 2, + next: { + value: 3, + next: { + value: 4, + next: null + } + } + } +}; + +function printList(list) { + + alert(list.value); // output the current item + + if (list.next) { + printList(list.next); // do the same for the rest of the list + } + +} + +printList(list); +``` + +Now what's better? + +Technically, the loop is more effective. These two variants do the same, but the loop does not spend resources for nested function calls. + +From the other side, the recursive variant is shorter and sometimes easier to understand. diff --git a/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/task.md b/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/task.md new file mode 100644 index 00000000..1076b952 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/task.md @@ -0,0 +1,29 @@ +importance: 5 + +--- + +# Output a single-linked list + +Let's say we have a single-linked list (as described in the chapter ): + +```js +let list = { + value: 1, + next: { + value: 2, + next: { + value: 3, + next: { + value: 4, + next: null + } + } + } +}; +``` + +Write a function `printList(list)` that outputs list items one-by-one. + +Make two variants of the solution: using a loop and using recursion. + +What's better: with recursion or without it? diff --git a/1-js/06-advanced-functions/01-recursion/05-output-single-linked-list-reverse/solution.md b/1-js/06-advanced-functions/01-recursion/05-output-single-linked-list-reverse/solution.md new file mode 100644 index 00000000..a9ba0baf --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/05-output-single-linked-list-reverse/solution.md @@ -0,0 +1,74 @@ +# Using a recursion + +The recursive logic is a little bit tricky here. + +We need to first output the rest of the list and *then* output the current one: + +```js run +let list = { + value: 1, + next: { + value: 2, + next: { + value: 3, + next: { + value: 4, + next: null + } + } + } +}; + +function printReverseList(list) { + + if (list.next) { + printReverseList(list.next); + } + + alert(list.value); +} + +printReverseList(list); +``` + +# Using a loop + +The loop variant is also a little bit more complicated then the direct output. + +There is no way to get the last value in our `list`. We also can't "go back". + +So what we can do is to first go through the items in the direct order and rememeber them in an array, and then output what we remembered in the reverse order: + +```js run +let list = { + value: 1, + next: { + value: 2, + next: { + value: 3, + next: { + value: 4, + next: null + } + } + } +}; + +function printReverseList(list) { + let arr = []; + let tmp = list; + + while (tmp) { + arr.push(tmp.value); + tmp = tmp.next; + } + + for (let i = arr.length - 1; i >= 0; i--) { + alert( arr[i] ); + } +} + +printReverseList(list); +``` + +Please note that the recursive solution actually does exactly the same: it follows the list, remembers the items in the chain of nested calls (in the execution context stack), and then outputs them. diff --git a/1-js/06-advanced-functions/01-recursion/05-output-single-linked-list-reverse/task.md b/1-js/06-advanced-functions/01-recursion/05-output-single-linked-list-reverse/task.md new file mode 100644 index 00000000..81b1f3e3 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/05-output-single-linked-list-reverse/task.md @@ -0,0 +1,9 @@ +importance: 5 + +--- + +# Output a single-linked list in the reverse order + +Output a single-linked list from the previous task in the reverse order. + +Make two solutions: using a loop and using a recursion. diff --git a/1-js/06-advanced-functions/01-recursion/article.md b/1-js/06-advanced-functions/01-recursion/article.md new file mode 100644 index 00000000..6995cdc4 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/article.md @@ -0,0 +1,534 @@ +# Recursion and stack + +Let's return to functions and study them more in-depth. + +Our first topic will be *recursion*. + +If you are not new to programming, then it is probably familiar and you could skip this chapter. + +Recursion is a programming pattern that is useful in situations when a task can be naturally split into several tasks of the same kind, but simpler. Or when a task can be simplified into an easy action plus a simpler variant of the same task. Or, as we'll see soon, to deal with certain data structures. + +When a function solves a task, in the process it can call many other functions. A partial case of this is when a function calls *itself*. That's called *recursion*. + +[cut] + +## Two ways of thinking + +For something simple to start with -- let's write a function `pow(x, n)` that raises `x` to a natural power of `n`. In other words, multiplies `x` by itself `n` times. + +```js +pow(2, 2) = 4 +pow(2, 3) = 8 +pow(2, 4) = 16 +``` + +There are two ways to implement it. + +1. Iterative thinking: the `for` loop: + + ```js run + function pow(x, n) { + let result = 1; + + // multiply result by x n times in the loop + for(let i = 0; i < n; i++) { + result *= x; + } + + return result; + } + + alert( pow(2, 3) ); // 8 + ``` + +2. Recursive thinking: simplify the task and call self: + + ```js run + function pow(x, n) { + if (n == 1) { + return x; + } else { + return x * pow(x, n - 1); + } + } + + alert( pow(2, 3) ); // 8 + ``` + +Please note how the recursive variant is fundamentally different. + +When `pow(x, n)` is called, the execution splits into two branches: + +```js + if n==1 = x + / +pow(x, n) = + \ + else = x * pow(x, n - 1) +``` + +1. If `n==1`, then everything is trivial. It is called *the base* of recursion, because it immediately produces the obvious result: `pow(x, 1)` equals `x`. +2. Otherwise, we can represent `pow(x, n)` as `x * pow(x, n-1)`. In maths, one would write xn = x * xn-1. This is called *a recursive step*: we transform the task into a simpler action (multiplication by `x`) and a simpler call of the same task (`pow` with lower `n`). Next steps simplify it further and further untill `n` reaches `1`. + +We can also say that `pow` *recursively calls itself* till `n == 1`. + +![recursive diagram of pow](recursion-pow.png) + + +For example, to calculate `pow(2, 4)` the recursive variant does these steps: + +1. `pow(2, 4) = 2 * pow(2, 3)` +2. `pow(2, 3) = 2 * pow(2, 2)` +3. `pow(2, 2) = 2 * pow(2, 1)` +4. `pow(2, 1) = 2` + +So, the recursion reduces a function call to a simpler one, and then -- to even more simpler, and so on, until the result becomes obvious. + +````smart header="Recursion is usually shorter" +A recursive solution is usually shorter than an iterative one. + +Here we can rewrite the same using the ternary `?` operator instead of `if` to make `pow(x, n)` more terse and still very readable: + +```js run +function pow(x, n) { + return (n == 1) ? x : (x * pow(x, n-1)); +} +``` +```` + +The maximal number of nested calls (including the first one) is called *recursion depth*. In our case, it will be exactly `n`. + +The maximal recursion depth is limited by JavaScript engine. We can make sure about 10000, some engines allow more, but 100000 is probably out of limit for the majority of them. There are automatic optimizations that help alleviate this ("tail calls optimizations"), but they are not yet supported everywhere and work only in simple cases. + +That limits the application of recursion, but it still remains very wide. There are many tasks where recursive way of thinking gives simpler code, easier to maintain. + +## The execution stack + +Now let's examine how recursive calls work. For that we'll look under the hood of functions. + +The information about a function run is stored in its *execution context*. + +The [execution context](https://tc39.github.io/ecma262/#sec-execution-contexts) is an internal data structure that contains details about the execution of a function: where the control flow is now, the current variables, the value of `this` (we don't use it here) and few other internal details. + +One function call has exactly one execution context associated with it. + +When a function makes a nested call, the following happens: + +- The current function is paused. +- The execution context associated with it is remembered in a special data structure called *execution context stack*. +- The nested call executes. +- After it ends, the old execution context is retrieved from the stack, and the outer function is resumed from where it stopped. + +Let's see what happens during the `pow(2, 3)` call. + +### pow(2, 3) + +In the beginning of the call `pow(2, 3)` the execution context will store variables: `x = 2, n = 3`, the execution flow is at line `1` of the function. + +We can sketch it as: + +
    +
  • + Context: { x: 2, n: 3, at line 1 } + pow(2, 3) +
  • +
+ +That's when the function starts to execute. The condition `n == 1` is falsy, so the flow continues into the second branch of `if`: + +```js run +function pow(x, n) { + if (n == 1) { + return x; + } else { +*!* + return x * pow(x, n - 1); +*/!* + } +} + +alert( pow(2, 3) ); +``` + + +The variables are same, but the line changes, so the context is now: + +
    +
  • + Context: { x: 2, n: 3, at line 5 } + pow(2, 3) +
  • +
+ +To calculate `x*pow(x, n-1)`, we need to make a subcall of `pow` with new arguments `pow(2, 2)`. + +### pow(2, 2) + +To do a nested call, JavaScript remembers the current execution context in the *execution context stack*. + +Here we call the same function `pow`, but it absolutely doesn't matter. The process is the same for all functions: + +1. The current context is "remembered" on top of the stack. +2. The new context is created for the subcall. +3. When the subcall is finished -- the previous context is popped from the stack, and its execution continues. + +Here's the context stack when we entered the subcall `pow(2, 2)`: + +
    +
  • + Context: { x: 2, n: 2, at line 1 } + pow(2, 2) +
  • +
  • + Context: { x: 2, n: 3, at line 5 } + pow(2, 3) +
  • +
+ +The new current execution context is on top (and bold), and previous remembered contexts are below. + +When we finish the subcall -- it is easy to resume the previous context, because it keeps both variables and the exact place of the code where it stopped. Here in the picture we use the word "line", but of course it's more precise. + +### pow(2, 1) + +The process repeats: a new subcall is made at line `5`, now with arguments `x=2`, `n=1`. + +A new execution context is created, the previous one is pushed on top of the stack: + +
    +
  • + Context: { x: 2, n: 1, at line 1 } + pow(2, 1) +
  • +
  • + Context: { x: 2, n: 2, at line 5 } + pow(2, 2) +
  • +
  • + Context: { x: 2, n: 3, at line 5 } + pow(2, 3) +
  • +
+ +There are 2 old contexts now and 1 currently running for `pow(2, 1)`. + +### The exit + +During the execution of `pow(2, 1)`, unlike before, the condition `n == 1` is truthy, so the first branch of `if` works: + +```js +function pow(x, n) { + if (n == 1) { +*!* + return x; +*/!* + } else { + return x * pow(x, n - 1); + } +} +``` + +There are no more nested calls, so the function finishes, returning `2`. + +As the function finishes, its execution context is not needed any more, so it's removed from the memory. The previous one is restored off the top of the stack: + + +
    +
  • + Context: { x: 2, n: 2, at line 5 } + pow(2, 2) +
  • +
  • + Context: { x: 2, n: 3, at line 5 } + pow(2, 3) +
  • +
+ +The execution of `pow(2, 2)` is resumed. It has the result of the subcall `pow(2, 1)`, so it also can finish the evaluation of `x * pow(x, n-1)`, returning `4`. + +Then the previous context is restored: + +
    +
  • + Context: { x: 2, n: 3, at line 5 } + pow(2, 3) +
  • +
+ +When it finishes, we have a result of `pow(2, 3) = 8`. + +The recursion depth in this case was: **3**. + +As we can see from the illustrations above, recursion depth equals the maximal number of context in the stack. + +Note the memory requirements. Contexts take memory. In our case, raising to the power of `n` actually requires the memory for `n` contexts, for all lower values of `n`. + +A loop-based algorithm is more memory-saving: + +```js +function pow(x, n) { + let result = 1; + + for(let i = 0; i < n; i++) { + result *= x; + } + + return result; +} +``` + +The iterative `pow` uses a single context changing `i` and `result` in the process. Its memory requirements are small, fixed and do not depend on `n`. + +**Any recursion can be rewritten as a loop. The loop variant usually can be made more effective.** + +...But sometimes the rewrite is non-trivial, especially when function uses different recursive subcalls depending on conditions and merges their results or when the branching is more intricate. And the optimization may be unneeded and totally not worth the efforts. + +Recursion can give a shorter code, easier to understand and support. Optimizations are not required in every place, mostly we need a good code, that's why it's used. + +## Recursive traversals + +Another great application of the recursion is a recursive traversal. + +Imagine, we have an company. The staff structure can be presented as an object: + +```js +let company = { + sales: [{ + name: 'John', + salary: 1000 + }, { + name: 'Alice', + salary: 600 + }], + + development: { + sites: [{ + name: 'Peter', + salary: 2000 + }, { + name: 'Alex', + salary: 1800 + }], + + internals: [{ + name: 'Jack', + salary: 1300 + }] + } +}; +``` + +In other words, a company has departments. + +- A department may have an array of staff. For instance, `sales` department has 2 employees: John and Alice. +- Or a department may split into subdepartments, like `development` has two branches: `sites` and `internals`. Each of them has the own staff. +- It is also possible that when a subdepartment grows, it divides into subsubdepartments (or teams). + + For instance, the `sites` department in the future may be split into teams for `siteA` and `siteB`. And they, potentially, can split even more. That's not on the picture, just something to have in mind. + +Now let's say we want a function to get the sum of all salaries. How can we do that? + +An iterative approach is not easy, because the structure is not simple. The first idea may be to make a `for` loop over `company` with nested subloop over 1st level departments. But then we need more nested subloops to iterate over the staff in 2nd level departments like `sites`. ...And then another subloop inside those for 3rd level departments that might appear in the future? Should we stop on level 3 or make 4 levels of loops? If we put 3-4 nested subloops in the code to traverse a single object, it becomes rather ugly. + +Let's try recursion. + +As we can see, when our function gets a department to sum, there are two possible cases: + +1. Either it's a "simple" department with an *array of people* -- then we can sum the salaries in a simple loop. +2. Or it's *an object with `N` subdepartments* -- then we can make `N` recursive calls to get the sum for each of the subdeps and combine the results. + +The (1) is the base of recursion, the trivial case. + +The (2) is the recursive step. A complex task is split into subtasks for smaller departments. They may in turn split again, but sooner or later the split will finish at (1). + +The 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 }], + development: { + sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800 }], + internals: [{name: 'Jack', salary: 1300}] + } +}; + +// The function to do the job +*!* +function sumSalaries(department) { + if (Array.isArray(department)) { // case (1) + return department.reduce((prev, current) => prev + current.salary, 0); // sum the array + } else { // case (2) + let sum = 0; + for(let subdep of Object.values(department)) { + sum += sumSalaries(subdep); // recursively call for subdepartments, sum the results + } + return sum; + } +} +*/!* + +alert(sumSalaries(company)); // 6700 +``` + +The code is short and easy to understand (hopefully?). That's the power of recursion. It also works for any level of subdepartment nesting. + +Here's the diagram of calls: + +![recursive salaries](recursive-salaries.png) + +We can easily see the principle: for an object `{...}` subcalls are made, while arrays `[...]` are the "leaves" of the recursion tree, they give immediate result. + +Note that the code uses smart features that we've covered before: + +- Method `arr.reduce` explained in the chapter to get the sum of the array. +- Loop `for(val of Object.values(obj))` to iterate over object values: `Object.values` returns an array of them. + + +## Recursive structures + +A recursive (recursively-defined) data structure is a structure that replicates itself in parts. + +We've just seen it in the example of a company structure above. + +A company *department* is: +- Either an array of people. +- Or an object with *departments*. + +For web-developers there are much better known examples: HTML and XML documents. + +In the HTML document, an *HTML-tag* may contain a list of: +- Text pieces. +- HTML-comments. +- Other *HTML-tags* (that in turn may contain text pieces/comments or other tags etc). + +That's once again a recursive definition. + +For better understanding, we'll cover one more recursive structure named "Linked list" that might be a better alternative for arrays in some cases. + +### Linked list + +Imagine, we want to store an ordered list of objects. + +The natural choice would be an array: + +```js +let arr = [obj1, obj2, obj3]; +``` + +...But there's a problem with arrays. The "delete element" and "insert element" operations are expensive. For instance, `arr.unshift(obj)` operation has to renumber all elements to make room for a new `obj`, and if the array is big, it takes time. Same with `arr.shift()`. + +The only structural modifications that do not require mass-renumbering are those that operate with the end of array: `arr.push/pop`. So an array can be quite slow for big queues. + +Alternatively, if we really need fast insertion/deletion, we can choose another data structure called a [linked list](https://en.wikipedia.org/wiki/Linked_list). + +The *linked list element* is recursively defined as an object with: +- `value`. +- `next` property referencing the next *linked list element* or `null` if that's the end. + +For instance: + +```js +let list = { + value: 1, + next: { + value: 2, + next: { + value: 3, + next: { + value: 4, + next: null + } + } + } +}; +``` + +Graphical representation of the list: + +![linked list](linked-list.png) + +An alternative code for creation: + +```js no-beautify +let list = { value: 1 }; +list.next = { value: 2 }; +list.next.next = { value: 3 }; +list.next.next.next = { value: 4 }; +``` + +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. + +The list can be easily split into multiple parts and later joined back: + +```js +let secondList = list.next.next; +list.next.next = null; +``` + +![linked list split](linked-list-split.png) + +To join: + +```js +list.next.next = secondList; +``` + +And surely we can insert or remove items in any place. + +For instance, to prepend a new value, we need to update the head of the list: + +```js +let list = { value: 1 }; +list.next = { value: 2 }; +list.next.next = { value: 3 }; +list.next.next.next = { value: 4 }; + +*!* +// prepend the new value to the list +list = { value: "new item", next: list }; +*/!* +``` + +![linked list](linked-list-0.png) + +To remove a value from the middle, change `next` of the previous one: + +```js +list.next = list.next.next; +``` + +![linked list](linked-list-remove-1.png) + +We made `list.next` jump over `1` to value `2`. The value `1` is now excluded from the chain. If it's not stored anywhere else, it will be automatically removed from the memory. + +Unlike arrays, there's no mass-renumbering, we can easily rearrange elements. + +Naturally, lists are not always better than arrays. Otherwise everyone would use only lists. + +The main drawback is that we can't easily access an element by its number. In an array that's easy: `arr[n]` is a direct reference. But in the list we need to start from the first item and go `next` `N` times to get the Nth element. + +...But we don't always need such operations. For instance, when we need a queue or even a [deque](https://en.wikipedia.org/wiki/Double-ended_queue) -- the ordered structure that must allow very fast adding/removing elements from both ends. + +Sometimes it's worth to add another variable named `tail` to track the last element of the list (and update it when adding/removing elements from the end). For large sets of elements the speed difference versus arrays is huge. + +## Summary + +Terms: +- *Recursion* is a programming term that means a "self-calling" function. Such functions can be used to solve certain tasks in elegant ways. + + When a function calls itself, that's called a *recursion step*. The *basis* of recursion is function arguments that make the task so simple that the function does not make further calls. + +- A [recursively-defined](https://en.wikipedia.org/wiki/Recursive_data_type) data structure is a data structure that can be defined using itself. + + For instance, the linked list can be defined as a data structure consisting of an object referencing a list (or null). + + ```js + list = { value, next -> list } + ``` + + Trees like HTML elements tree or the department tree from this chapter are also naturally recursive: they branch and every branch can have other branches. + + Recursive functions can be used to walk them as we've seen in the `sumSalary` example. + +Any recursive function can be rewritten into an iterative one. And that's sometimes required to optimize stuff. But for many tasks a recursive solution is fast enough and easier to write and support. diff --git a/1-js/06-advanced-functions/01-recursion/head.html b/1-js/06-advanced-functions/01-recursion/head.html new file mode 100644 index 00000000..c2ac3e8c --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/head.html @@ -0,0 +1,26 @@ + \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-0.png b/1-js/06-advanced-functions/01-recursion/linked-list-0.png new file mode 100644 index 00000000..000a80da Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/linked-list-0.png differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-0@2x.png b/1-js/06-advanced-functions/01-recursion/linked-list-0@2x.png new file mode 100644 index 00000000..5a236869 Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/linked-list-0@2x.png differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.png b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.png new file mode 100644 index 00000000..477989ad Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.png differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1@2x.png b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1@2x.png new file mode 100644 index 00000000..41de7661 Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1@2x.png differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-split.png b/1-js/06-advanced-functions/01-recursion/linked-list-split.png new file mode 100644 index 00000000..ac220349 Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/linked-list-split.png differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-split@2x.png b/1-js/06-advanced-functions/01-recursion/linked-list-split@2x.png new file mode 100644 index 00000000..201c66f1 Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/linked-list-split@2x.png differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list.png b/1-js/06-advanced-functions/01-recursion/linked-list.png new file mode 100644 index 00000000..64b6fb2b Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/linked-list.png differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list@2x.png b/1-js/06-advanced-functions/01-recursion/linked-list@2x.png new file mode 100644 index 00000000..c28fa825 Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/linked-list@2x.png differ diff --git a/1-js/06-advanced-functions/01-recursion/recursion-pow.png b/1-js/06-advanced-functions/01-recursion/recursion-pow.png new file mode 100644 index 00000000..30577f89 Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/recursion-pow.png differ diff --git a/1-js/06-advanced-functions/01-recursion/recursion-pow@2x.png b/1-js/06-advanced-functions/01-recursion/recursion-pow@2x.png new file mode 100644 index 00000000..c1997342 Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/recursion-pow@2x.png differ diff --git a/1-js/06-advanced-functions/01-recursion/recursive-salaries.png b/1-js/06-advanced-functions/01-recursion/recursive-salaries.png new file mode 100644 index 00000000..2b901540 Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/recursive-salaries.png differ diff --git a/1-js/06-advanced-functions/01-recursion/recursive-salaries@2x.png b/1-js/06-advanced-functions/01-recursion/recursive-salaries@2x.png new file mode 100644 index 00000000..261ab144 Binary files /dev/null and b/1-js/06-advanced-functions/01-recursion/recursive-salaries@2x.png differ diff --git a/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md b/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md new file mode 100644 index 00000000..eaa42ab9 --- /dev/null +++ b/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md @@ -0,0 +1,247 @@ +# Rest parameters and spread operator + +Many JavaScript built-in functions support on arbitrary number of arguments. + +For instance: + +- `Math.max(arg1, arg2, ..., argN)` -- returns the greatest of the arguments. +- `Object.assign(dest, src1, ..., srcN)` -- copies properties from `src1..N` into `dest`. +- ...and so on. + +In this chapter we'll see how to do the same. And, more important, how to feel comfortable working with such functions and arrays. + +[cut] + +## Rest parameters `...` + +A function can be called with any number of arguments, no matter how it is defined. + +Like here: +```js run +function sum(a, b) { + return a + b; +} + +alert( sum(1, 2, 3, 4, 5) ); +``` + +There will be no error because of "excessive" arguments. But of course in the result only the first two will be counted. + +The rest parameters can be mentioned in a function definition with three dots `...`. They literally mean: "gather the remaining parameters into an array". + +For instance, to gather all arguments into array `args`: + +```js run +function sumAll(...args) { // args is the name for the array + let sum = 0; + + for(let arg of args) sum += arg; + + return sum; +} + +alert( sumAll(1) ); // 1 +alert( sumAll(1, 2) ); // 3 +alert( sumAll(1, 2, 3) ); // 6 +``` + +We can choose to get first parameters as variables, and gather only the rest. + +Here the first two arguments go into variables and the rest goes to `titles` array: + +```js run +function showName(firstName, lastName, ...titles) { + alert( firstName + ' ' + lastName ); // Julius Caesar + + // the rest go into titles array + // i.e. titles = ["Consul", "Imperator"] + alert( titles[0] ); // Consul + alert( titles[1] ); // Imperator + alert( titles.length ); // 2 +} + +showName("Julius", "Caesar", "Consul", "Imperator"); +``` + +````warn header="The rest parameters must be at the end" +The rest parameters gather all remaining arguments, so the following has no sense: + +```js +function f(arg1, ...rest, arg2) { // arg2 after ...rest ?! + // error +} +``` + +The `...rest` must always be the last. +```` + +## The "arguments" variable + +There is also a special array-like object named `arguments` that contains all arguments by their index. + +For instance: + +```js run +function showName() { + alert( arguments.length ); + alert( arguments[0] ); + alert( arguments[1] ); + + // it's iterable + // for(let arg of arguments) alert(arg); +} + +// shows: 2, Julius, Caesar +showName("Julius", "Caesar"); + +// shows: 1, Ilya, undefined (no second argument) +showName("Ilya"); +``` + +In old times, rest parameters did not exist in the language, and `arguments` was the only way to get all arguments of the function no matter of their total number. + +And it still works, we can use it. + +But the downside is that although `arguments` is both array-like and iterable, it's not an array. It does not support array methods, so we can't say call `arguments.map(...)`. + +Also, it always has all arguments in it, we can't capture them partially, like we did with rest parameters. + +So when we need these features, then rest parameters are preferred. + +````smart header="Arrow functions do not have `\"arguments\"`" +If we access the `arguments` object from an arrow function, it takes them from the outer "normal" function. + +Here's an example: + +```js run +function f() { + let showArg = () => alert(arguments[0]); + showArg(); +} + +f(1); // 1 +``` +As we remember, arrow functions don't have their own `this`. Now we know they don't have the special `arguments` object either. + +```` + +## Spread operator [#spread-operator] + +We've just seen how to get an array from the list of parameters. + +But sometimes we need to do exactly the reverse. + +For instance, there's a built-in function [Math.max](mdn:js/Math/max) that returns the greatest number from the list: + +```js run +alert( Math.max(3, 5, 1) ); // 5 +``` + +Now let's say we have an array `[3, 5, 1]`. How to call `Math.max` with it? + +Passing it "as it" won't work, because `Math.max` expects a list of numeric arguments, not a single array: + +```js run +let arr = [3, 5, 1]; + +*!* +alert( Math.max(arr) ); // NaN +*/!* +``` + +...And surely we can't manually list items in the code `Math.max(arg[0], arg[1], arg[2])`, because we may be unsure how much are there. As our script executes, there might be many, or there might be none. Also that would be ugly. + +*Spread operator* to the rescue. It looks similar to rest parameters, also using `...`, but does quite the opposite. + +When `...arr` is used in the function call, it "expands" an iterable object `arr` into the list of arguments. + +For `Math.max`: + +```js run +let arr = [3, 5, 1]; + +alert( Math.max(...arr) ); // 5 (spread turns array into a list of arguments) +``` + +We also can pass multiple iterables this way: + +```js run +let arr1 = [1, -2, 3, 4]; +let arr2 = [8, 3, -8, 1]; + +alert( Math.max(...arr1, ...arr2) ); // 8 +``` + +...And even combine the spread operator with normal values: + + +```js run +let arr1 = [1, -2, 3, 4]; +let arr2 = [8, 3, -8, 1]; + +alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25 +``` + +Also spread operator can be used to merge arrays: + +```js run +let arr = [3, 5, 1]; +let arr2 = [8, 9, 15]; + +*!* +let merged = [0, ...arr, 2, ...arr2]; +*/!* + +alert(merged); // 0,3,5,1,2,8,9,15 (0, then arr, then 2, then arr2) +``` + +In the examples above we used an array to demonstrate the spread operator, but any iterable will do. + +For instance, here we use spread operator to turn the string into array of characters: + +```js run +let str = "Hello"; + +alert( [...str] ); // H,e,l,l,o +``` + +The spread operator internally uses iterators to gather elements, the same way as `for..of` does. + +So, for a string, `for..of` returns characters and `...str` becomes `"h","e","l","l","o"`. The list of characters is passed to array initializer `[...str]`. + +For this particular task we could also use `Array.from`, because it converts an iterable (like a string) into an array: + +```js run +let str = "Hello"; + +// Array.from converts an iterable into an array +alert( Array.from(str) ); // H,e,l,l,o +``` + +The result is the same as `[...str]`. + +But there's a subtle difference between `Array.from(obj)` and `[...obj]`: + +- `Array.from` operates on both array-likes and iterables. +- The spread operator operates only on iterables. + +So, for the task of turning something into an array, `Array.from` appears more universal. + + +## Summary + +When we see `"..."` in the code, it is either rest parameters or the spread operator. + +There's an easy way to distinguish between them: + +- When `...` is at the end of function parameters, it's "rest parameters" and gathers the rest of the list into the array. +- When `...` occurs in a function call or alike, it's called a "spread operator" and expands an array into the list. + +Use patterns: + +- Rest parameters are used to create functions that accept any number of arguments. +- The spread operator is used to pass an array to functions that normally require a list of many arguments. + +Together they help to travel between a list and an array of parameters with ease. + +All arguments of a function call are also available in "old-style" `arguments`: array-like iterable object. diff --git a/1-js/06-advanced-functions/03-closure/1-counter-independent/solution.md b/1-js/06-advanced-functions/03-closure/1-counter-independent/solution.md new file mode 100644 index 00000000..cff69a25 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/1-counter-independent/solution.md @@ -0,0 +1,5 @@ +The answer: **0,1.** + +Functions `counter` and `counter2` are created by different invocations of `makeCounter`. + +So they have independent outer Lexical Environments, each one has it's own `count`. diff --git a/1-js/06-advanced-functions/03-closure/1-counter-independent/task.md b/1-js/06-advanced-functions/03-closure/1-counter-independent/task.md new file mode 100644 index 00000000..e8c17dd3 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/1-counter-independent/task.md @@ -0,0 +1,31 @@ +importance: 5 + +--- + +# Are counters independent? + +Here we make two counters: `counter` and `counter2` using the same `makeCounter` function. + +Are they independent? What is the second counter going to show? `0,1` or `2,3` or something else? + +```js +function makeCounter() { + let count = 0; + + return function() { + return count++; + }; +} + +let counter = makeCounter(); +let counter2 = makeCounter(); + +alert( counter() ); // 0 +alert( counter() ); // 1 + +*!* +alert( counter2() ); // ? +alert( counter2() ); // ? +*/!* +``` + diff --git a/1-js/06-advanced-functions/03-closure/2-counter-object-independent/solution.md b/1-js/06-advanced-functions/03-closure/2-counter-object-independent/solution.md new file mode 100644 index 00000000..cd4e641e --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/2-counter-object-independent/solution.md @@ -0,0 +1,24 @@ + +Surely it will work just fine. + +Both nested functions are created within the same outer Lexical Environment, so they share access to the same `count` variable: + +```js run +function Counter() { + let count = 0; + + this.up = function() { + return ++count; + }; + + this.down = function() { + return --count; + }; +} + +let counter = new Counter(); + +alert( counter.up() ); // 1 +alert( counter.up() ); // 2 +alert( counter.down() ); // 1 +``` diff --git a/1-js/06-advanced-functions/03-closure/2-counter-object-independent/task.md b/1-js/06-advanced-functions/03-closure/2-counter-object-independent/task.md new file mode 100644 index 00000000..d770b0ff --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/2-counter-object-independent/task.md @@ -0,0 +1,29 @@ +importance: 5 + +--- + +# Counter object + +Here a counter object is made with the help of the constructor function. + +Will it work? What will it show? + +```js +function Counter() { + let count = 0; + + this.up = function() { + return ++count; + }; + this.down = function() { + return --count; + }; +} + +let counter = new Counter(); + +alert( counter.up() ); // ? +alert( counter.up() ); // ? +alert( counter.down() ); // ? +``` + diff --git a/1-js/06-advanced-functions/03-closure/3-function-in-if/solution.md b/1-js/06-advanced-functions/03-closure/3-function-in-if/solution.md new file mode 100644 index 00000000..e2e7a91b --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/3-function-in-if/solution.md @@ -0,0 +1,3 @@ +The result is **an error**. + +The function `sayHi` is declared inside the `if`, so it only lives inside it. There is no `sayHi` outside. \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md b/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md new file mode 100644 index 00000000..d0dbbeb1 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md @@ -0,0 +1,20 @@ + +# Function in if + +Look at the code. What will be result of the call at the last line? + +```js run +let phrase = "Hello"; + +if (true) { + let user = "John"; + + function sayHi() { + alert(`${phrase}, ${user}`); + } +} + +*!* +sayHi(); +*/!* +``` diff --git a/1-js/06-advanced-functions/03-closure/4-closure-sum/solution.md b/1-js/06-advanced-functions/03-closure/4-closure-sum/solution.md new file mode 100644 index 00000000..e8c8c465 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/4-closure-sum/solution.md @@ -0,0 +1,17 @@ +For the second brackets to work, the first ones must return a function. + +Like this: + +```js run +function sum(a) { + + return function(b) { + return a + b; // takes "a" from the outer lexical environment + }; + +} + +alert( sum(1)(2) ); // 3 +alert( sum(5)(-1) ); // 4 +``` + diff --git a/1-js/06-advanced-functions/03-closure/4-closure-sum/task.md b/1-js/06-advanced-functions/03-closure/4-closure-sum/task.md new file mode 100644 index 00000000..c2f3eabe --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/4-closure-sum/task.md @@ -0,0 +1,17 @@ +importance: 4 + +--- + +# Sum with closures + +Write function `sum` that works like this: `sum(a)(b) = a+b`. + +Yes, exactly this way, via double brackets (not a mistype). + +For instance: + +```js +sum(1)(2) = 3 +sum(5)(-1) = 4 +``` + diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/solution.js b/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/solution.js new file mode 100644 index 00000000..66a149d9 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/solution.js @@ -0,0 +1,8 @@ + +function inArray(arr) { + return x => arr.includes(x); +} + +function inBetween(a, b) { + return x => (x >= a && x <= b); +} \ No newline at end of file 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/6-filter-through-function/_js.view/source.js new file mode 100644 index 00000000..74989df2 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/source.js @@ -0,0 +1,10 @@ + +let arr = [1, 2, 3, 4, 5, 6, 7]; + +function inBetween(a, b) { + // ...your code... +} + +function inArray(arr) { + // ...your code... +} 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/6-filter-through-function/_js.view/test.js new file mode 100644 index 00000000..86d2d3b4 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/test.js @@ -0,0 +1,21 @@ + +describe("inArray", function() { + let arr = [1, 2, 3, 4, 5, 6, 7]; + + it("returns the filter for values in array", function() { + + let filter = inArray(arr); + assert.isTrue(filter(5)); + assert.isFalse(filter(0)); + }); +}); + + +describe("inBetween", function() { + + it("returns the filter for values between", function() { + let filter = inBetween(3, 6); + assert.isTrue(filter(5)); + assert.isFalse(filter(0)); + }); +}); diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/solution.md b/1-js/06-advanced-functions/03-closure/6-filter-through-function/solution.md new file mode 100644 index 00000000..5bbc33b0 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/6-filter-through-function/solution.md @@ -0,0 +1,26 @@ + +# Filter inBetween + +```js run +function inBetween(a, b) { + return function(x) { + return x >= a && x <= b; + }; +} + +let arr = [1, 2, 3, 4, 5, 6, 7]; +alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6 +``` + +# Filter inArray + +```js run +function inArray(arr) { + return function(x) { + return arr.includes(x); + }; +} + +let arr = [1, 2, 3, 4, 5, 6, 7]; +alert( arr.filter(inArray([1, 2, 10])) ); // 1,2 +``` diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/task.md b/1-js/06-advanced-functions/03-closure/6-filter-through-function/task.md new file mode 100644 index 00000000..d1c39f94 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/6-filter-through-function/task.md @@ -0,0 +1,29 @@ +importance: 5 + +--- + +# Filter through function + +We have a built-in method `arr.filter(f)` for arrays. It filters all elements through the function `f`. If it returns `true`, then that element is returned in the resulting array. + +Make a set of "ready to use" filters: + +- `inBetween(a, b)` -- between `a` and `b` or equal to them (inclusively). +- `inArray([...])` -- in the given array. + +The usage must be like this: + +- `arr.filter(inBetween(3,6))` -- selects only values between 3 and 6. +- `arr.filter(inArray([1,2,3]))` -- selects only elements matching with one of the members of `[1,2,3]`. + +For instance: + +```js +/* .. your code for inBetween and inArray */ +let arr = [1, 2, 3, 4, 5, 6, 7]; + +alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6 + +alert( arr.filter(inArray([1, 2, 10])) ); // 1,2 +``` + 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 new file mode 100644 index 00000000..bd57085e --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/7-sort-by-field/solution.md @@ -0,0 +1,22 @@ + + +```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/7-sort-by-field/task.md b/1-js/06-advanced-functions/03-closure/7-sort-by-field/task.md new file mode 100644 index 00000000..08fb5cc3 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/7-sort-by-field/task.md @@ -0,0 +1,36 @@ +importance: 5 + +--- + +# Sort by field + +We've got an array of objects to sort: + +```js +let users = [ + { name: "John", age: 20, surname: "Johnson" }, + { name: "Pete", age: 18, surname: "Peterson" }, + { name: "Ann", age: 19, surname: "Hathaway" } +]; +``` + +The usual way to do that would be: + +```js +// by name (Ann, John, Pete) +users.sort((a, b) => a.name > b.name ? 1 : -1); + +// by age (Pete, Ann, John) +users.sort((a, b) => a.age > b.age ? 1 : -1); +``` + +Can we make it even less verbose, like this? + +```js +users.sort(byField('name')); +users.sort(byField('age')); +``` + +So, instead of writing a function, just put `byField(fieldName)`. + +Write the function `byField` that can be used for that. 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/8-make-army/_js.view/solution.js new file mode 100644 index 00000000..a26578ae --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/8-make-army/_js.view/solution.js @@ -0,0 +1,13 @@ +function makeArmy() { + + let shooters = []; + + for(let i = 0; i < 10; i++) { + let shooter = function() { // shooter function + alert( i ); // should show its number + }; + shooters.push(shooter); + } + + return shooters; +} 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/8-make-army/_js.view/source.js new file mode 100644 index 00000000..7c7aaa1e --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/8-make-army/_js.view/source.js @@ -0,0 +1,22 @@ +function makeArmy() { + let shooters = []; + + let i = 0; + while (i < 10) { + let shooter = function() { // shooter function + alert( i ); // should show its number + }; + shooters.push(shooter); + i++; + } + + return shooters; +} + +/* +let army = makeArmy(); + +army[0](); // the shooter number 0 shows 10 +army[5](); // and number 5 also outputs 10... +// ... all shooters show 10 instead of their 0, 1, 2, 3... +*/ 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/8-make-army/_js.view/test.js new file mode 100644 index 00000000..b61e6e4d --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/8-make-army/_js.view/test.js @@ -0,0 +1,25 @@ +describe("army", function() { + + let army; + + before(function() { + army = makeArmy(); + window.alert = sinon.stub(window, "alert"); + }); + + it("army[0] shows 0", function() { + army[0](); + assert(alert.calledWith(0)); + }); + + + it("army[5] shows 5", function() { + army[5](); + assert(alert.calledWith(5)); + }); + + after(function() { + window.alert.restore(); + }); + +}); diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.png b/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.png new file mode 100644 index 00000000..d51e8167 Binary files /dev/null and b/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.png differ diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy@2x.png b/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy@2x.png new file mode 100644 index 00000000..e70edbd6 Binary files /dev/null and b/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy@2x.png differ diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/solution.md b/1-js/06-advanced-functions/03-closure/8-make-army/solution.md new file mode 100644 index 00000000..03c34b07 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/8-make-army/solution.md @@ -0,0 +1,121 @@ + +Let's examine what's done inside `makeArmy`, and the solution will become obvious. + +1. It creates an empty array `shooters`: + + ```js + let shooters = []; + ``` +2. Fills it in the loop via `shooters.push(function...)`. + + Every element is a function, so the resulting array looks like this: + + ```js no-beautify + shooters = [ + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); } + ]; + ``` + +3. The array is returned from the function. + +Then, later, the call to `army[5]()` will get the element `army[5]` from the array (it will be a function) and call it. + +Now why all such functions show the same? + +That's because there's no local variable `i` inside `shooter` functions. When such a function is called, it takes `i` from its outer lexical environment. + +What will be the value of `i`? + +If we look at the source: + +```js +function makeArmy() { + ... + let i = 0; + while (i < 10) { + let shooter = function() { // shooter function + alert( i ); // should show its number + }; + ... + } + ... +} +``` + +...We can see that it lives in the lexical environment associated with the current `makeArmy()` run. But when `army[5]()` is called, `makeArmy` has already finished its job, and `i` has the last value: `10` (the end of `while`). + +As a result, all `shooter` functions get from the outer lexical envrironment the same, last value `i=10`. + +The fix can be very simple: + +```js run +function makeArmy() { + + let shooters = []; + +*!* + for(let i = 0; i < 10; i++) { +*/!* + let shooter = function() { // shooter function + alert( i ); // should show its number + }; + shooters.push(shooter); + } + + return shooters; +} + +let army = makeArmy(); + +army[0](); // 0 +army[5](); // 5 +``` + +Now it works correctly, because every time the code block in `for (..) {...}` is executed, a new Lexical Environment is created for it, with the corresponding value of `i`. + +So, the value of `i` now lives a little bit closer. Not in `makeArmy()` Lexical Environment, but in the Lexical Environment that corresponds the current loop iteration. A `shooter` gets the value exactly from the one where it was created. + +![](lexenv-makearmy.png) + +Here we rewrote `while` into `for`. + +Another trick could be possible, let's see it for better understanding of the subject: + + +```js run +function makeArmy() { + let shooters = []; + + let i = 0; + while (i < 10) { +*!* + let j = i; +*/!* + let shooter = function() { // shooter function + alert( *!*j*/!* ); // should show its number + }; + shooters.push(shooter); + i++; + } + + return shooters; +} + +let army = makeArmy(); + +army[0](); // 0 +army[5](); // 5 +``` + +The `while` loop, just like `for`, makes a new Lexical Environment for each run. So here we make sure that it gets the right value for a `shooter`. + +We copy `let j = i`. This makes a loop body local `j` and copies the value of `i` to it. Primitives are copied "by value", so we actually get a complete independent copy of `i`, belonging to the current loop iteration. diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/task.md b/1-js/06-advanced-functions/03-closure/8-make-army/task.md new file mode 100644 index 00000000..ede8fd04 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/8-make-army/task.md @@ -0,0 +1,35 @@ +importance: 5 + +--- + +# Army of functions + +The following code creates an array of `shooters`. + +Every function is meant to output its number. But something is wrong... + +```js run +function makeArmy() { + let shooters = []; + + let i = 0; + while (i < 10) { + let shooter = function() { // shooter function + alert( i ); // should show its number + }; + shooters.push(shooter); + i++; + } + + return shooters; +} + +let army = makeArmy(); + +army[0](); // the shooter number 0 shows 10 +army[5](); // and number 5 also outputs 10... +// ... all shooters show 10 instead of their 0, 1, 2, 3... +``` + +Why all shooters show the same? Fix the code so that they work as intended. + diff --git a/1-js/06-advanced-functions/03-closure/article.md b/1-js/06-advanced-functions/03-closure/article.md new file mode 100644 index 00000000..7e3b14db --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/article.md @@ -0,0 +1,644 @@ + +# Closure + +JavaScript is a very function-oriented language. It gives a lot of freedom. A function can be created at one moment, then copied to another variable or passed as an argument to another function and called from a totally different place later. + +We know that a function can access variables outside of it. And this feature is used quite often. + +But what happens when an outer variables changes? Does a function get a most recent value or the one that existed when the function was created? + +Also, what happens when a function travels to another place of the code and is called from there -- does it get access to outer variables in the new place? + +Different languages behave differently here, in this chapter we cover JavaScript. + +[cut] + +## A couple of questions + +Let's formulate two questions for the seed, and then study internal mechanics piece-by-piece, so that you'll be able to answer these questions and more complex ones in the future. + +1. The function `sayHi` uses an external variable `name`. When the function runs, which value of these two it's 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 in both 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 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 outer variables from its creation place or the invocation place or maybe 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" technically is. + +In JavaScript, every running function, code block and the script as a whole have an associated object named *Lexical Environment*. + +The Lexical Environment object consists of two parts: + +1. *Environment Record* -- an object that has all local variables as its properties (and some other information like the value of `this`). +2. A reference to the *outer lexical environment*, usually the one associated with the code lexically right outside of it (outside of the current figure brackets).a + +So, a "variable" is just a property of the special internal object, Environment Record. "To get or change a variable" means "to get or change the property of that object". + +For instance, in this simple code, there is only one Lexical Environment: + +![lexical environment](lexical-environment-global.png) + +This is a so-called global Lexical Environment, associated with the whole script. For browsers, all ` + ``` + + Here, first two alerts use the current window, and the latter two take variables from `iframe` window. Can be any variables if `iframe` originates from the same protocol/host/port. + +## "this" and global object + +Sometimes, the value of `this` is exactly the global object. That's rarely used, but some scripts rely on that. + +1. In the browser, the value of `this` in the global area is `window`: + + ```js run + // outside of functions + alert( this === window ); // true + ``` + + Other, non-browser environments, may use another value for `this` in such cases. + +2. When a function with `this` is called in non-strict mode, it gets the global object as `this`: + ```js run no-strict + // not in strict mode (!) + function f() { + alert(this); // [object Window] + } + + f(); // called without an object + ``` + + By specification, `this` in this case must be the global object, even in non-browser environments like Node.JS. That's for compatibility with old scripts, in strict mode `this` would be `undefined`. diff --git a/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/_js.view/solution.js b/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/_js.view/solution.js new file mode 100644 index 00000000..ce894698 --- /dev/null +++ b/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/_js.view/solution.js @@ -0,0 +1,13 @@ +function makeCounter() { + let count = 0; + + function counter() { + return count++; + } + + counter.set = value => count = value; + + counter.decrease = () => count--; + + return counter; +} diff --git a/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/_js.view/source.js b/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/_js.view/source.js new file mode 100644 index 00000000..5bf29aa2 --- /dev/null +++ b/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/_js.view/source.js @@ -0,0 +1,18 @@ +function makeCounter() { + let count = 0; + + // ... your code ... +} + +let counter = makeCounter(); + +alert( counter() ); // 0 +alert( counter() ); // 1 + +counter.set(10); // set the new count + +alert( counter() ); // 10 + +counter.decrease(); // decrease the count by 1 + +alert( counter() ); // 10 (instead of 11) diff --git a/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/_js.view/test.js b/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/_js.view/test.js new file mode 100644 index 00000000..0e613aba --- /dev/null +++ b/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/_js.view/test.js @@ -0,0 +1,41 @@ +describe("counter", function() { + + it("increases from call to call", function() { + + let counter = makeCounter(); + + assert.equal( counter(), 0 ); + assert.equal( counter(), 1 ); + assert.equal( counter(), 2 ); + }); + + + describe("counter.set", function() { + it("sets the count", function() { + + let counter = makeCounter(); + + counter.set(10); + + assert.equal( counter(), 10 ); + assert.equal( counter(), 11 ); + }); + }); + + describe("counter.decrease", function() { + it("decreases the count", function() { + + let counter = makeCounter(); + + counter.set(10); + + assert.equal( counter(), 10 ); + + counter.decrease(); + + assert.equal( counter(), 10 ); + + }); + }); + +}); \ No newline at end of file diff --git a/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/solution.md b/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/solution.md new file mode 100644 index 00000000..e829d96e --- /dev/null +++ b/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/solution.md @@ -0,0 +1,2 @@ + +The solution uses `count` in the local variable, but addition methods are written right into the `counter`. They share the same outer lexical environment and also can access the current `count`. diff --git a/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/task.md b/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/task.md new file mode 100644 index 00000000..e34bb67b --- /dev/null +++ b/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/task.md @@ -0,0 +1,15 @@ +importance: 5 + +--- + +# Set and decrease for counter + +Modify the code of `makeCounter()` so that the counter can also decrease and set the number: + +- `counter()` should return the next number (as before). +- `counter.set(value)` should set the `count` to `value`. +- `counter.decrease(value)` should decrease the `count` by 1. + +See the sandbox code for the complete usage example. + +P.S. You can use either a closure or the function property to keep the current count. Or write both variants. diff --git a/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/solution.md b/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/solution.md new file mode 100644 index 00000000..5c932691 --- /dev/null +++ b/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/solution.md @@ -0,0 +1,55 @@ + +1. For the whole thing to work *anyhow*, the result of `sum` must be function. +2. That function must keep in memory the current value between calls. +3. According to the task, the function must become the number when used in `==`. Functions are objects, so the conversion happens as described in the chapter , and we can provide our own method that returns the number. + +Now the code: + +```js run +function sum(a) { + + let currentSum = a; + + function f(b) { + currentSum += b; + return f; + } + + f.toString = function() { + return currentSum; + }; + + return f; +} + +alert( sum(1)(2) ); // 3 +alert( sum(5)(-1)(2) ); // 6 +alert( sum(6)(-1)(-2)(-3) ); // 0 +alert( sum(0)(1)(2)(3)(4)(5) ); // 15 +``` + +Please note that the `sum` function actually works only once. It returns function `f`. + +Then, on each subsequent call, `f` adds its parameter to the sum `currentSum`, and returns itself. + +**There is no recursion in the last line of `f`.** + +Here is what recursion looks like: + +```js +function f(b) { + currentSum += b; + return f(); // <-- recursive call +} +``` + +And in our case, we just return the function, without calling it: + +```js +function f(b) { + currentSum += b; + return f; // <-- does not call itself, returns itself +} +``` + +This `f` will be used in the next call, again return itself, so many times as needed. Then, when used as a number or a string -- the `toString` returns the `currentSum`. We could also use `Symbol.toPrimitive` or `valueOf` here for the conversion. diff --git a/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/task.md b/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/task.md new file mode 100644 index 00000000..dc13f260 --- /dev/null +++ b/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/task.md @@ -0,0 +1,17 @@ +importance: 2 + +--- + +# Sum with an arbitrary amount of brackets + +Write function `sum` that would work like this: + +```js +sum(1)(2) == 3; // 1 + 2 +sum(1)(2)(3) == 6; // 1 + 2 + 3 +sum(5)(-1)(2) == 6 +sum(6)(-1)(-2)(-3) == 0 +sum(0)(1)(2)(3)(4)(5) == 15 +``` + +P.S. Hint: you may need to setup custom object to primitive conversion for your function. \ No newline at end of file diff --git a/1-js/06-advanced-functions/06-function-object/article.md b/1-js/06-advanced-functions/06-function-object/article.md new file mode 100644 index 00000000..fb9a5798 --- /dev/null +++ b/1-js/06-advanced-functions/06-function-object/article.md @@ -0,0 +1,354 @@ + +# Function object, NFE + +As we already know, functions in JavaScript are values. + +Every value in JavaScript has the type. What type of value is a function? + +In JavaScript, a function is an object. + +A good way to imagine functions is as callable "action objects". We can not only call them, but also treat them as objects: add/remove properties, pass by reference etc. + + +## The "name" property + +Function objects contain few sometimes-useable properties. + +For instance, a function name is accessible as the "name" property: + +```js run +function sayHi() { + alert("Hi"); +} + +alert(sayHi.name); // sayHi +``` + +What's more funny, the name-assigning logic is smart. It also sticks the right name to function that are used in assignments: + +```js run +let sayHi = function() { + alert("Hi"); +} + +alert(sayHi.name); // sayHi (works!) +``` + +Also works if the assignment is done via a default value: + +```js run +function f(sayHi = function() {}) { + alert(sayHi.name); // sayHi (works!) +} + +f(); +``` + +In the specification, this feature is called a "contextual name". If the function does not provide one, then in an assignment it is figured out from the context. + +Object methods have names too: + +```js run +let user = { + + sayHi() { + // ... + }, + + sayBye: function() { + // ... + } + +} + +alert(user.sayHi.name); // sayHi +alert(user.sayBye.name); // sayBye +``` + +There's no magic though. There are cases when there's no way to figure out the right name. + +Then it's empty, like here: + +```js +// function created inside array +let arr = [function() {}]; + +alert( arr[0].name ); // +// the engine has no way to set up the right name, so there is none +``` + +In practice, most functions do have a name. + +## The "length" property + +There is another built-in property "length" that returns the number of function parameters, for instance: + +```js run +function f1(a) {} +function f2(a, b) {} +function many(a, b, ...more) {} + +alert(f1.length); // 1 +alert(f2.length); // 2 +alert(many.length); // 2 +``` + +Here we can see that rest parameters are not counted. + +The `length` property is sometimes used for introspection in functions that operate on other functions. + +For instance, in the code below `ask` function accepts a `question` to ask and an arbitrary number of `handler` functions to call. + +When a user answers, it calls the handlers. We can pass two kinds of handlers: + +- A zero-argument function, then it is only called for a positive answer. +- A function with arguments, then it is called in any case and gets the answer. + +The idea is that we have a simple no-arguments handler syntax for positive cases (most frequent variant), but allow to provide universal handlers as well. + +To call `handlers` the right way, we examine the `length` property: + +```js run +function ask(question, ...handlers) { + let isYes = confirm(question); + + for(let handler of handlers) { + if (handler.length == 0) { + if (isYes) handler(); + } else { + handler(isYes); + } + } + +} + +// for positive answer, both handlers are called +// for negative answer, only the second one +ask("Question?", () => alert('You said yes'), result => alert(result)); +``` + +This is a particular case of so-called [polymorphism](https://en.wikipedia.org/wiki/Polymorphism_(computer_science)) -- treating arguments differently depending on their type or, in our case depending on the `length`. The idea does have a use in JavaScript libraries. + +## Custom properties + +We can also add properties of our own. + +Here we add the `counter` property to track the total calls count: + +```js run +function sayHi() { + alert("Hi"); + + *!* + // let's count how many times we run + sayHi.counter++; + */!* +} +sayHi.counter = 0; // initial value + +sayHi(); // Hi +sayHi(); // Hi + +alert( `Called ${sayHi.counter} times` ); // Called 2 times +``` + +```warn header="A property is not a variable" +A property assigned to a function like `sayHi.counter = 0` does *not* define a local variable `counter` inside it. In other words, a property `counter` and a variable `let counter` are two unrelated things. + +We can treat a function as an object, store properties in it, but that has no effect on its execution. Variables never use function properties and vice versa. These are just parallel words. +``` + +Function properties can replace the closure sometimes. For instance, we can rewrite the counter example from the chapter to use a function property: + +```js run +function makeCounter() { + // instead of: + // let count = 0 + + function counter() { + return counter.count++; + }; + + counter.count = 0; + + return counter; +} + +let counter = makeCounter(); +alert( counter() ); // 0 +alert( counter() ); // 1 +``` + +The `count` is now stored in the function directly, not in its outer Lexical Environment. + +Is it worse or better than using the closure? + +The main difference is that if the value of `count` lives in an outer variable, then an external code is unable to access it. Only nested functions may modify it. And if it's bound to function, then such thing is possible: + +```js run +function makeCounter() { + + function counter() { + return counter.count++; + }; + + counter.count = 0; + + return counter; +} + +let counter = makeCounter(); + +*!* +counter.count = 10; +alert( counter() ); // 10 +*/!* +``` + +So it depends on our aims which variant to choose. + +## Named Function Expression + +Named Function Expression or, shortly, NFE, is a term for Function Expressions that have a name. + +For instance, let's take an ordinary Function Expression: + +```js +let sayHi = function(who) { + alert(`Hello, ${who}`); +}; +``` + +...And add a name to it: + +```js +let sayHi = function *!*func*/!*(who) { + alert(`Hello, ${who}`); +}; +``` + +Did we do anything sane here? What's the role of that additional `"func"` name? + +First let's note, that we still have a Function Expression. Adding the name `"func"` after `function` did not make it a Function Declaration, because it is still created as a part of an assignment expression. + +Adding such a name also did not break anything. + +The function is still available as `sayHi()`: + +```js run +let sayHi = function *!*func*/!*(who) { + alert(`Hello, ${who}`); +}; + +sayHi("John"); // Hello, John +``` + +There are two special things about the name `func`: + +1. It allows to reference the function from inside itself. +2. It is not visible outside of the function. + +For instance, the function `sayHi` below re-calls itself with `"Guest"` if no `who` is provided: + +```js run +let sayHi = function *!*func*/!*(who) { + if (who) { + alert(`Hello, ${who}`); + } else { +*!* + func("Guest"); // use func to re-call itself +*/!* + } +}; + +sayHi(); // Hello, Guest + +// But this won't work: +func(); // Error, func is not defined (not visible outside of the function) +``` + +Why do we use `func`? Maybe just use `sayHi` for the nested call? + + +Actually, in most cases we can: + +```js +let sayHi = function(who) { + if (who) { + alert(`Hello, ${who}`); + } else { +*!* + sayHi("Guest"); +*/!* + } +}; +``` + +The problem with that code is that the value of `sayHi` may change. The function may go to another variable, and the code will start to give errors: + +```js run +let sayHi = function(who) { + if (who) { + alert(`Hello, ${who}`); + } else { +*!* + sayHi("Guest"); // Error: sayHi is not a function +*/!* + } +}; + +let welcome = sayHi; +sayHi = null; + +welcome(); // Error, the nested sayHi call doesn't work any more! +``` + +That happens because the function takes `sayHi` from its outer lexical environment. There's no local `sayHi`, so the outer variable is used. And at the moment of the call that outer `sayHi` is `null`. + +The optional name which we can put into the Function Expression is exactly meant to solve this kind of problems. + +Let's use it to fix the code: + +```js run +let sayHi = function *!*func*/!*(who) { + if (who) { + alert(`Hello, ${who}`); + } else { +*!* + func("Guest"); // Now all fine +*/!* + } +}; + +let welcome = sayHi; +sayHi = null; + +welcome(); // Hello, Guest (nested call works) +``` + +Now it works, because the name `"func"` is function-local. It is not taken from outside (and not visible there). The specification guarantees that it always references the current function. + +The outer code still has it's variable `sayHi` or `welcome` later. And `func` is an "internal function name", how it calls itself privately. + +```smart header="There's no such thing for Function Declaration" +The "internal name" feature described here is only available for Function Expressions, not to Function Declarations. For Function Declarations, there's just no syntax possibility to add a one more "internal" name. + +Sometimes, when we need a reliable internal name, it's the reason to rewrite a Function Declaration to Named Function Expression form. +``` + +## Summary + +Functions are objects. + +Here we covered their properties: + +- `name` -- the function name. Exists not only when given in the function definition, but also for assignments and object properties. +- `length` -- the number of arguments in the function definition. Rest parameters are not counted. + +If the function is declared as a Function Expression (not in the main code flow), and it carries the name, then it is called Named Function Expression. The name can be used inside to reference itself, for recursive calls or such. + +Also, functions may carry additional properties. Many well-known JavaScript libraries make a great use of this feature. + +They create a "main" function and attach many other "helper" functions to it. For instance, the [jquery](https://jquery.com) library creates a function named `$`. The [lodash](https://lodash.com) library creates a function `_`. And then adds `_.clone`, `_.keyBy` and other properties to (see the [docs](https://lodash.com/docs) when you want learn more about them). Actually, they do it to less pollute the global space, so that a single library gives only one global variable. That lowers the chance of possible naming conflicts. + +So, a function can do a useful job by itself and also carry a bunch of other functionality in properties. diff --git a/1-js/06-advanced-functions/07-new-function/article.md b/1-js/06-advanced-functions/07-new-function/article.md new file mode 100644 index 00000000..b44da634 --- /dev/null +++ b/1-js/06-advanced-functions/07-new-function/article.md @@ -0,0 +1,139 @@ + +# The "new Function" syntax + +There's one more way to create a function. It's rarely used, but sometimes there's no alternative. + +[cut] + +## The syntax + +The syntax for creating a function: + +```js +let func = new Function('a', 'b', 'return a + b'); +``` + +All arguments of `new Function` are strings. Parameters go first, and the body is the last. + +For instance: + +```js run +let sum = new Function('arg1', 'arg2', 'return arg1 + arg2'); + +alert( sum(1, 2) ); // 3 +``` + +If there are no arguments, then there will be only body: + +```js run +let sayHi = new Function('alert("Hello")'); + +sayHi(); // Hello +``` + +The major difference from other ways we've seen -- the function is created literally from a string, that is passed at run time. + +All previous declarations required us, programmers, to write the function code in the script. + +But `new Function` allows to turn any string into a function, for example we can receive a new function from the server and then execute it: + +```js +let str = ... receive the code from the server dynamically ... + +let func = new Function(str); +func(); +``` + +It is used in very specific cases, like when we receive the code from the server, or to dynamically compile a function from a template. The need for that usually arises at advanced stages of development. + +## The closure + +Usually, a function remembers where it was born in the special property `[[Environment]]`. It references the Lexical Environment from where it's created. + +But when a function is created using `new Function`, its `[[Environment]]` references not the current Lexical Environment, but instead the global one. + +```js run + +function getFunc() { + let value = "test"; + +*!* + let func = new Function('alert(value)'); +*/!* + + return func; +} + +getFunc()(); // error: value is not defined +``` + +Compare it with the regular behavior: + +```js run +function getFunc() { + let value = "test"; + +*!* + let func = function() { alert(value); }; +*/!* + + return func; +} + +getFunc()(); // *!*"test"*/!*, from the Lexical Environment of getFunc +``` + +This special feature of `new Function` looks strange, but appears very useful in practice. + +Imagine that we really have to create a function from the string. The code of that function is not known at the time of writing the script (that's why we don't use regular functions), but will be known in the process of execution. We may receive it from the server or from another source. + +Our new function needs to interact with the main script. + +Maybe we want it to be able to access outer local variables? + +But the problem is that before JavaScript is published to production, it's compressed using a *minifier* -- a special program that shrinks code by removing extra comments, spaces and -- what's important, renames local variables into shorter ones. + +For instance, if a function has `let userName`, minifier replaces it `let a` (or another letter if this one is occupied), and does it everywhere. That's usually a safe thing to do, because the variable is local, nothing outside the function can access it. And inside the function minifier replaces every mention of it. Minifiers are smart, they analyze the code structure, not just find-and-replace, so that's ok. + +...But if `new Function` could access outer variables, then it would be unable to find `userName`. + +**Even if we could access outer lexical environment in `new Function`, we would have problems with minifiers.** + +The "special feature" of `new Function` saves us from mistakes. + +And it enforces better code. If we need to pass something to a function, created by `new Function`, we should pass it explicitly as arguments. + +The "sum" function actually does that right: + +```js run +*!* +let sum = new Function('a', 'b', ' return a + b; '); +*/!* + +let a = 1, b = 2; + +*!* +// outer values are passed as arguments +alert( sum(a, b) ); // 3 +*/!* +``` + +## Summary + +The syntax: + +```js +let func = new Function(arg1, arg2, ..., body); +``` + +For historical reasons, arguments can also be given as a comma-separated list. + +These three mean the same: + +```js +new Function('a', 'b', ' return a + b; '); // basic syntax +new Function('a,b', ' return a + b; '); // comma-separated +new Function('a , b', ' return a + b; '); // comma-separated with spaces +``` + +Functions created with `new Function`, have `[[Environment]]` referencing the global Lexical Environment, not the outer one. Hence, they can not use outer variables. But that's actually good, because it saves us from errors. Explicit parameters passing is a much better thing architecturally and has no problems with minifiers. \ No newline at end of file diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/solution.md b/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/solution.md new file mode 100644 index 00000000..13f01deb --- /dev/null +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/solution.md @@ -0,0 +1,42 @@ + +Using `setInterval`: + +```js run +function printNumbers(from, to) { + let current = from; + + let timerId = setInterval(function() { + alert(current); + if (current == to) { + clearInterval(timerId); + } + current++; + }, 1000); +} + +// usage: +printNumbers(5, 10); +``` + +Using recursive `setTimeout`: + + +```js run +function printNumbers(from, to) { + let current = from; + + setTimeout(function go() { + alert(current); + if (current < to) { + setTimeout(go, 1000); + } + current++; + }, 1000); +} + +// usage: +printNumbers(5, 10); +``` + +Note that in both solutions, there is an initial delay before the first output. Sometimes we need to add a line to make the first output immediately, that's easy to do. + diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/task.md b/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/task.md new file mode 100644 index 00000000..87e723c6 --- /dev/null +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/task.md @@ -0,0 +1,13 @@ +importance: 5 + +--- + +# Output every second + +Write a function `printNumbers(from, to)` that outputs a number every second, starting from `from` and ending with `to`. + +Make two variants of the solution. + +1. Using `setInterval`. +2. Using recursive `setTimeout`. + diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/3-rewrite-settimeout/solution.md b/1-js/06-advanced-functions/08-settimeout-setinterval/3-rewrite-settimeout/solution.md new file mode 100644 index 00000000..42a6a004 --- /dev/null +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/3-rewrite-settimeout/solution.md @@ -0,0 +1,23 @@ + + +```js run +let i = 0; + +let start = Date.now(); + +let timer = setInterval(count, 0); + +function count() { + + for(let j = 0; j < 1000000; j++) { + i++; + } + + if (i == 1000000000) { + alert("Done in " + (Date.now() - start) + 'ms'); + cancelInterval(timer); + } + +} +``` + diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/3-rewrite-settimeout/task.md b/1-js/06-advanced-functions/08-settimeout-setinterval/3-rewrite-settimeout/task.md new file mode 100644 index 00000000..3c788170 --- /dev/null +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/3-rewrite-settimeout/task.md @@ -0,0 +1,32 @@ +importance: 4 + +--- + +# Rewrite setTimeout with setInterval + +Here's the function that uses nested `setTimeout` to split a job into pieces. + +Rewrite it to `setInterval`: + +```js run +let i = 0; + +let start = Date.now(); + +function count() { + + if (i == 1000000000) { + alert("Done in " + (Date.now() - start) + 'ms'); + } else { + setTimeout(count, 0); + } + + // a piece of heavy job + for(let j = 0; j < 1000000; j++) { + i++; + } + +} + +count(); +``` diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/4-settimeout-result/solution.md b/1-js/06-advanced-functions/08-settimeout-setinterval/4-settimeout-result/solution.md new file mode 100644 index 00000000..e652a3b3 --- /dev/null +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/4-settimeout-result/solution.md @@ -0,0 +1,15 @@ + +Any `setTimeout` will run only after the current code has finished. + +The `i` will be the last one: `100000000`. + +```js run +let i = 0; + +setTimeout(() => alert(i), 100); // 100000000 + +// assume that the time to execute this function is >100ms +for(let j = 0; j < 100000000; j++) { + i++; +} +``` diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/4-settimeout-result/task.md b/1-js/06-advanced-functions/08-settimeout-setinterval/4-settimeout-result/task.md new file mode 100644 index 00000000..faca4600 --- /dev/null +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/4-settimeout-result/task.md @@ -0,0 +1,27 @@ +importance: 5 + +--- + +# What will setTimeout show? + +In the code below there's a `setTimeout` call scheduled, then a heavy calculation is run, that takes more than 100ms to finish. + +When the scheduled function will run? + +1. After the loop. +2. Before the loop. +3. In the beginning of the loop. + + +What `alert` is going to show? + +```js +let i = 0; + +setTimeout(() => alert(i), 100); // ? + +// assume that the time to execute this function is >100ms +for(let j = 0; j < 100000000; j++) { + i++; +} +``` diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/article.md b/1-js/06-advanced-functions/08-settimeout-setinterval/article.md new file mode 100644 index 00000000..1cb92015 --- /dev/null +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/article.md @@ -0,0 +1,464 @@ +# Scheduling: setTimeout and setInterval + +We may decide to execute a function not right now, but at a certain time later. That's called "scheduling a call". + +There are two methods for it: + +- `setTimeout` allows to run a function once after the interval of time. +- `setInterval` allows to run a function regularly with the interval between the runs. + +These methods are not a part of JavaScript specification. But most environments have the internal scheduler and provide these methods. In particular, they are supported in all browsers and Node.JS. + + +[cut] + +## setTimeout + +The syntax: + +```js +let timerId = setTimeout(func|code, delay[, arg1, arg2...]) +``` + +Parameters: + +`func|code` +: Function or a string of code to execute. +Usually, that's a function. For historical reasons, a string of code can be passed, but that's not recommended. + +`delay` +: The delay before run, in milliseconds (1000 ms = 1 second). + +`arg1`, `arg2`... +: Arguments for the function (not supported in IE9-) + +For instance, this code calls `sayHi()` after one second: + +```js run +function sayHi() { + alert('Hello'); +} + +*!* +setTimeout(sayHi, 1000); +*/!* +``` + +With arguments: + +```js run +function sayHi(phrase, who) { + alert( phrase + ', ' + who ); +} + +*!* +setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John +*/!* +``` + +If the first argument is a string, then JavaScript creates a function from it. + +So, this will also work: + +```js run no-beautify +setTimeout("alert('Hello')", 1000); +``` + +But using strings is not recommended, use functions instead of them, like this: + +```js run no-beautify +setTimeout(() => alert('Hello'), 1000); +``` + +````smart header="Pass a function, but don't run it" +Novice developers sometimes make a mistake by adding brackets `()` after the function: + +```js +// wrong! +setTimeout(sayHi(), 1000); +``` +That doesn't work, because `setTimeout` expects a reference to function. And here `sayHi()` runs the function, and the *result of its execution* is passed to `setTimeout`. In our case the result of `sayHi()` is `undefined` (the function returns nothing), so nothing is scheduled. +```` + +### Canceling with clearTimeout + +A call to `setTimeout` returns a "timer identifier" `timerId` that we can use to cancel the execution. + +The syntax to cancel: + +```js +let timerId = setTimeout(...); +clearTimeout(timerId); +``` + +In the code below we schedule the function and then cancel it (changed our mind). As a result, nothing happens: + +```js run no-beautify +let timerId = setTimeout(() => alert("never happens"), 1000); +alert(timerId); // timer identifier + +clearTimeout(timerId); +alert(timerId); // same identifier (doesn't become null after canceling) +``` + +As we can see from `alert` output, in a browser the timer identifier is a number. In other environments, that can be something else. For instance, Node.JS returns a timer object with additional methods. + +Again, there is no universal specification for these methods, so that's fine. + +For browsers, timers are described in the [timers section](https://www.w3.org/TR/html5/webappapis.html#timers) of HTML5 standard. + +## setInterval + +Method `setInterval` has the same syntax as `setTimeout`: + +```js +let timerId = setInterval(func|code, delay[, arg1, arg2...]) +``` + +All arguments have the same meaning. But unlike `setTimeout` it runs the function not only once, but regularly after the given interval of time. + +To stop further calls, we should call `clearInterval(timerId)`. + +The following example will show the message every 2 seconds. After 5 seconds, the output is stopped: + +```js run +// repeat with the interval of 2 seconds +let timerId = setInterval(() => alert('tick'), 2000); + +// after 5 seconds stop +setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000); +``` + +```smart header="Modal windows freeze time in Chrome/Opera/Safari" +In browsers IE and Firefox the internal timer continues "ticking" while showing `alert/confirm/prompt`, but in Chrome, Opera and Safari the internal timer becomes "frozen". + +So if you run the code above and don't dismiss the `alert` window for some time, then in Firefox/IE next `alert` will be shown immediately as you do it (2 seconds passed from the previous invocation), and in Chrome/Opera/Safari -- after 2 more seconds (timer did not tick during the `alert`). +``` + +## Recursive setTimeout + +There are two ways of running something regularly. + +One is `setInterval`. The other one is a recursive `setTimeout`, like this: + +```js +/** instead of: +let timerId = setInterval(() => alert('tick'), 2000); +*/ + +let timerId = setTimeout(function tick() { + alert('tick'); +*!* + timerId = setTimeout(tick, 2000); // (*) +*/!* +}, 2000); +``` + +The `setTimeout` above schedules next call right at the end of the current one `(*)`. + +Recursive `setTimeout` is more flexible method than `setInterval`. This way the next call may be scheduled differently, depending on the results of the current one. + +For instance, we need to write a service that each 5 seconds sends a request to server asking for data, but in case the server is overloaded, it should increase the interval to 10, 20, 40 seconds... + +Here's the pseudocode: +```js +let delay = 5000; + +let timerId = setTimeout(function request() { + ...send request... + + if (request failed due to server overload) { + // increase the interval to the next run + delay *= 2; + } + + timerId = setTimeout(tick, delay); + +}, delay); +``` + + +And if we regulary have CPU-hungry tasks, then we can measure the time taken by the execution and plan the next call sooner or later. + +**Recursive `setTimeout` guarantees a delay between the executions, `setInterval` -- does not.** + +Let's compare two code fragments. The first one uses `setInterval`: + +```js +let i = 1; +setInterval(function() { + func(i); +}, 100); +``` + +The second one uses recursive `setTimeout`: + +```js +let i = 1; +setTimeout(function run() { + func(i); + setTimeout(run, 100); +}, 100); +``` + +For `setInterval` the internal scheduler will run `func(i)` every 100ms: + +![](setinterval-interval.png) + +Did you notice?... + +**The real delay between `func` calls for `setInterval` is less than in the code!** + +That's natural, because the time taken by `func` execution "consumes" a part of the interval. + +It is possible that `func` execution turns out to be longer than we expected and takes more than 100ms. + +In this case the engine waits for `func` to complete, then checks the scheduler and if the time is up, then runs it again *immediately*. + +In the edge case, if the function always executes longer than `delay` ms, then the calls will happen without pause at all. + +And here is the picture for recursive `setTimeout`: + +![](settimeout-interval.png) + +**Recursive `setTimeout` guarantees the fixed delay (here 100ms).** + +That's because a new call is planned at the end of the previous one. + +````smart header="Garbage collection" +When a function is passed in `setInterval/setTimeout`, an internal reference is created to it and saved in the scheduler. It prevents the function from being garbage collected, even if there are no other references to it. + +```js +// the function stays in memory until the scheduler calls it +setTimeout(function() {...}, 100); +``` + +For `setInterval` the function stays in memory until `cancelInterval` is called. + +There's a side-effect. A function references the outer lexical environment, so, while it lives, outer variables live too. They may take much more memory than the function itself. So when we don't need the scheduled function any more, it's better to cancel it, even if it's very small. +```` + +## setTimeout(...,0) + +There's a special use case: `setTimeout(func, 0)`. + +This schedules the execution of `func` as soon as possible. But scheduler will invoke it only after the current code is complete. + +So the function is scheduled to run "right after" the current code. In other words, *asynchronously*. + +For instance, this outputs "Hello", then immediately "World": + +```js run +setTimeout(() => alert("World"), 0); + +alert("Hello"); +``` + +The first line "puts the call into calendar after 0ms". But the scheduler will only "check the calendar" after the current code is complete, so `"Hello"` is first, and `"World"` -- after it. + +### Splitting CPU-hungry tasks + +There's a trick to split CPU-hungry task using `setTimeout`. + +For instance, syntax highlighting script (used to colorize code examples on this page) is quite CPU-heavy. To hightlight the code, it performs the analysis, creates many colored elements, adds them to the document -- for a big text that takes a lot. It may even cause the browser to "hang", that's unacceptable. + +So we can split the long text to pieces. First 100 lines, then plan another 100 lines using `setTimeout(...,0)`, and so on. + +For clarity, let's take a simpler example for consideration. We have a function to count from `1` to `1000000000`. + +If you run it, the CPU will hang. For server-side JS that's clearly noticeable, and if you are running it in-browser, then try to click other buttons on the page -- you'll see that whole JavaScript actually is paused, no other actions work until it finishes. + +```js run +let i = 0; + +let start = Date.now(); + +function count() { + + // do a heavy job + for(let j = 0; j < 1e9; j++) { + i++; + } + + alert("Done in " + (Date.now() - start) + 'ms'); +} + +count(); +``` + +The browser may even show "the script takes too long" warning (but hopefully won't, the number is not very big). + +Let's split the job using the nested `setTimeout`: + +```js run +let i = 0; + +let start = Date.now(); + +function count() { + + // do a piece of the heavy job (*) + do { + i++; + } while (i % 1e6 != 0); + + if (i == 1e9) { + alert("Done in " + (Date.now() - start) + 'ms'); + } else { + setTimeout(count, 0); // schedule the new call (**) + } + +} + +count(); +``` + +Now the browser UI is fully functional during the "counting" process. + +We do a part of the job `(*)`: + +1. First run: `i=1...1000000`. +2. Second run: `i=1000001..2000000`. +3. ...and so on, the `while` checks if `i` is evenly divided by `100000`. + +Then the next call is scheduled in `(*)` if we're not done yet. + +Pauses between `count` executions provide just enough "breath" for the JavaScript engine to do something else, to react on other user actions. + +The notable thing is that both variants: with and without splitting the job by `setInterval` -- are comparable in speed. There's no much difference in the overall counting time. + +To make them closer let's make an improvement. + +We'll move the scheduling in the beginning of the `count()`: + +```js run +let i = 0; + +let start = Date.now(); + +function count() { + + // move the scheduling at the beginning + if (i < 1e9 - 1e6) { + setTimeout(count, 0); // schedule the new call + } + + do { + i++; + } while (i % 1e6 != 0); + + if (i == 1e9) { + alert("Done in " + (Date.now() - start) + 'ms'); + } + +} + +count(); +``` + +Now when we start to `count()` and know that we'll need to `count()` more -- we schedule that immediately, before doing the job. + +If you run it, easy to notice that it takes significantly less time. + +````smart header="Minimal delay of nested timers in-browser" +In the browser, there's a limitation of how often nested timers can run. The [HTML5 standard](https://www.w3.org/TR/html5/webappapis.html#timers) says: "after five nested timers..., the interval is forced to be at least four milliseconds.". + +Let's demonstrate what it means by the example below. The `setTimeout` call in it re-schedules itself after `0ms`. Each call remembers the real time from the previous one in the `times` array. What the real delays look like? Let's see: + +```js run +let start = Date.now(); +let times = []; + +setTimeout(function run() { + times.push(Date.now() - start); // remember delay from the previous call + + if (start + 100 < Date.now()) alert(times); // show the delays after 100ms + else setTimeout(run, 0); // else re-schedule +}, 0); + +// an example of the output: +// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100 +``` + +First timers run immediately (just as written in the spec), and then the delay comes into play and we see `9, 15, 20, 24...`. + +That limitation comes from ancient times and many scripts rely on it, so it exists for historical reasons. + +For server-side JavaScript, that limitation does not exist, and there exist other ways to schedule an immediate asynchronous job, like [process.nextTick](https://nodejs.org/api/process.html) and [setImmediate](https://nodejs.org/api/timers.html) for Node.JS. So the notion is browser-specific only. +```` + +### Allowing the browser to render + +Another benefit for in-browser scripts is that they can show a progress bar or something to the user. That's because the browser usually does all "repainting" after the script is complete. + +So if we do a single huge function then even if it changes something, the changes are not reflected in the document till it finishes. + +Here's the demo: +```html run +
+ + +``` + +If you run it, the changes to `i` will show up after the whole count finishes. + +And if we use `setTimeout` to split it into pieces then changes are applied in-between the runs, so this looks better: + +```html run +
+ + +``` + +Now the `
` shows increasing values of `i`. + +## Summary + +- Methods `setInterval(func, delay, ...args)` and `setTimeout(func, delay, ...args)` allow to run the `func` regularly/once after `delay` milliseconds. +- To cancel the execution, we should call `clearInterval/clearTimeout` with the value returned by `setInterval/setTimeout`. +- Nested `setTimeout` calls is a more flexible alternative to `setInterval`. Also they can guarantee the minimal time *between* the executions. +- Zero-timeout scheduling `setTimeout(...,0)` is used to schedule the call "as soon as possible, but after the current code is complete". + +Some use cases of `setTimeout(...,0)`: +- To split CPU-hungry tasks into pieces, so that the script doesn't "hang" +- To let the browser do something else while the process is going on (paint the progress bar). + +Please note that all scheduling methods do not *guarantee* the exact delay. We should not rely on that in the scheduled code. + +For example, the in-browser timer may slow down for a lot of reasons: +- The CPU is overloaded. +- The browser tab is in the background mode. +- The laptop is on battery. + +All that may decrease the minimal timer resolution (the minimal delay) to 300ms or even 1000ms depending on the browser and settings. diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.png b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.png new file mode 100644 index 00000000..060b2c29 Binary files /dev/null and b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.png differ diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval@2x.png b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval@2x.png new file mode 100644 index 00000000..4071849c Binary files /dev/null and b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval@2x.png differ diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.png b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.png new file mode 100644 index 00000000..6c473a33 Binary files /dev/null and b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.png differ diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval@2x.png b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval@2x.png new file mode 100644 index 00000000..dd45e324 Binary files /dev/null and b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval@2x.png differ diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/_js.view/solution.js b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/_js.view/solution.js new file mode 100644 index 00000000..9ef50370 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/_js.view/solution.js @@ -0,0 +1,11 @@ +function spy(func) { + + function wrapper(...args) { + wrapper.calls.push(args); + return func.apply(this, arguments); + } + + wrapper.calls = []; + + return wrapper; +} \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/_js.view/source.js b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/_js.view/source.js new file mode 100644 index 00000000..38da0105 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/_js.view/source.js @@ -0,0 +1,5 @@ +function spy(func) { + // your code +} + + diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/_js.view/test.js b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/_js.view/test.js new file mode 100644 index 00000000..7e84f3c9 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/_js.view/test.js @@ -0,0 +1,44 @@ +describe("spy", function() { + it("records calls into its property", function() { + function work() {} + + work = spy(work); + assert.deepEqual(work.calls, []); + + work(1, 2); + assert.deepEqual(work.calls, [ + [1, 2] + ]); + + work(3, 4); + assert.deepEqual(work.calls, [ + [1, 2], + [3, 4] + ]); + }); + + it("transparently wraps functions", function() { + + let sum = sinon.spy((a, b) => a + b); + + let wrappedSum = spy(sum); + + assert.equal(wrappedSum(1, 2), 3); + assert(spy.calledWith(1, 2)); + }); + + + it("transparently wraps methods", function() { + + let calc = { + sum: sinon.spy((a, b) => a + b) + }; + + calc.wrappedSum = spy(calc.sum); + + assert.equal(calculator.wrappedSum(1, 2), 3); + assert(spy.calledWith(1, 2)); + assert(spy.calledOn(calculator)); + }); + +}); \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/solution.md b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/solution.md new file mode 100644 index 00000000..4a79d3e1 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/solution.md @@ -0,0 +1 @@ +Here we can use `log.push(args)` to store all arguments in the log and `f.apply(this, args)` to forward the call. diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/task.md b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/task.md new file mode 100644 index 00000000..e4a30cb8 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/task.md @@ -0,0 +1,30 @@ +importance: 5 + +--- + +# Spy decorator + +Create a decorator `spy(func)` that should return a wrapper that saves all calls to function in its `calls` property. + +Every call is saved as an array of arguments. + +For instance: + +```js +function work(a, b) { + alert( a + b ); // work is an arbitrary function or method +} + +*!* +work = spy(work); +*/!* + +work(1, 2); // 3 +work(4, 5); // 9 + +for(let args of work.calls) { + alert( 'call:' + args.join() ); // "call:1,2", "call:4,5" +} +``` + +P.S. That decorator is sometimes useful for unit-testing, it's advanced form is `sinon.spy` in [Sinon.JS](http://sinonjs.org/) library. diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/_js.view/solution.js b/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/_js.view/solution.js new file mode 100644 index 00000000..127cff98 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/_js.view/solution.js @@ -0,0 +1,7 @@ +function delay(f, ms) { + + return function() { + setTimeout(() => f.apply(this, arguments), ms); + }; + +}; \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/_js.view/test.js b/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/_js.view/test.js new file mode 100644 index 00000000..d9295da5 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/_js.view/test.js @@ -0,0 +1,46 @@ +describe("delay", function() { + before(function() { + this.clock = sinon.useFakeTimers(); + }); + + after(function() { + this.clock.restore(); + }); + + it("calls the function after the specified timeout", function() { + let start = Date.now(); + + function f(x) { + assert.equal(Date.now() - start, 1000); + } + f = sinon.spy(f); + + let f1000 = delay(f, 1000); + f1000("test"); + this.clock.tick(2000); + assert(f.calledOnce, 'calledOnce check fails'); + }); + + it("passes arguments and this", function() { + let start = Date.now(); + let user = { + sayHi: function(phrase, who) { + assert.equal(this, user); + assert.equal(phrase, "Hello"); + assert.equal(who, "John"); + assert.equal(Date.now() - start, 1500); + } + }; + + user.sayHi = sinon.spy(user.sayHi); + + let spy = user.sayHi; + user.sayHi = delay(user.sayHi, 1500); + + user.sayHi("Hello", "John"); + + this.clock.tick(2000); + + assert(spy.calledOnce, 'calledOnce check failed'); + }); +}); diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/solution.md b/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/solution.md new file mode 100644 index 00000000..44b5024e --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/solution.md @@ -0,0 +1,29 @@ +The solution: + +```js +function delay(f, ms) { + + return function() { + setTimeout(() => f.apply(this, arguments), ms); + }; + +} +``` + +Please note how an arrow function is used here. As we know, arrow functions do not have own `this` and `arguments`, so `f.apply(this, arguments)` takes `this` and `arguments` from the wrapper. + +If we pass a regular function, `setTimeout` would call it without arguments and `this=window` (in-browser), so we'd need to write a bit more code to pass them from the wrapper: + +```js +function delay(f, ms) { + + // added variables to pass this and arguments from the wrapper inside setTimeout + return function(...args) { + let savedThis = this; + setTimeout(function() { + f.apply(savedThis, args); + }, ms); + }; + +} +``` diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/task.md b/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/task.md new file mode 100644 index 00000000..c04c68d7 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/task.md @@ -0,0 +1,26 @@ +importance: 5 + +--- + +# Delaying decorator + +Create a decorator `delay(f, ms)` that delays each call of `f` by `ms` milliseconds. + +For instance: + +```js +function f(x) { + alert(x); +} + +// create wrappers +let f1000 = delay(f, 1000); +let f1500 = delay(f, 1500); + +f1000("test"); // shows "test" after 1000ms +f1500("test"); // shows "test" after 1500ms +``` + +In other words, `delay(f, ms)` returns a "delayed by `ms`" variant of `f`. + +In the code above, `f` is a function of a single argument, but your solution should pass all arguments and the context `this`. 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 new file mode 100644 index 00000000..065a77d1 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/solution.js @@ -0,0 +1,15 @@ +function debounce(f, ms) { + + let isCooldown = false; + + return function() { + if (isCooldown) return; + + f.apply(this, arguments); + + isCooldown = true; + + setTimeout(() => isCooldown = false, 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 new file mode 100644 index 00000000..16dc171e --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/test.js @@ -0,0 +1,41 @@ +describe("debounce", function() { + before(function() { + this.clock = sinon.useFakeTimers(); + }); + + after(function() { + this.clock.restore(); + }); + + it("calls the function at maximum once in ms milliseconds", function() { + let log = ''; + + function f(a) { + log += a; + } + + f = debounce(f, 1000); + + f(1); // runs at once + f(2); // ignored + + setTimeout(() => f(3), 100); // ignored (too early) + setTimeout(() => f(4), 1100); // runs (1000 ms passed) + setTimeout(() => f(5), 1500); // ignored (less than 1000 ms from the last run) + + this.clock.tick(5000); + assert.equal(log, "14"); + }); + + it("keeps the context of the call", function() { + let obj = { + f() { + assert.equal(this, obj); + } + }; + + obj.f = debounce(obj.f, 1000); + obj.f("test"); + }); + +}); \ 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 new file mode 100644 index 00000000..1516aca3 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/solution.md @@ -0,0 +1,30 @@ + + +```js run no-beautify +function debounce(f, ms) { + + let isCooldown = false; + + return function() { + if (isCooldown) return; + + f.apply(this, arguments); + + isCooldown = true; + + setTimeout(() => isCooldown = false, ms); + }; + +} +``` + +The call to `debounce` returns a wrapper. There may be two states: + +- `isCooldown = false` -- ready to run. +- `isCooldown = true` -- waiting for the timeout. + +In the first call `isCooldown` is falsy, so the call proceeds, and the state changes to `true`. + +While `isCooldown` is true, all other calls are ignored. + +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 new file mode 100644 index 00000000..466c6bc3 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/task.md @@ -0,0 +1,24 @@ +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. + +In other words, when we call a "debounced" function, it guarantees that all other future in the closest `ms` milliseconds will be ignored. + +For instance: + +```js no-beautify +let f = debounce(alert, 1000); + +f(1); // runs immediately +f(2); // 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) +``` + +In practice `debounce` is useful for functions that retrieve/update something when we know that nothing new can be done in such a short period of time, so it's better not to waste resources. \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/_js.view/solution.js b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/_js.view/solution.js new file mode 100644 index 00000000..8071be9d --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/_js.view/solution.js @@ -0,0 +1,34 @@ +function throttle(func, ms) { + + let isThrottled = false, + savedArgs, + savedThis; + + function wrapper() { + + if (isThrottled) { + // memo last arguments to call after the cooldown + savedArgs = arguments; + savedThis = this; + return; + } + + // otherwise go to cooldown state + func.apply(this, arguments); + + isThrottled = true; + + // plan to reset isThrottled after the delay + setTimeout(function() { + isThrottled = false; + if (savedArgs) { + // if there were calls, savedThis/savedArgs have the last one + // recursive call runs the function and sets cooldown again + wrapper.apply(savedThis, savedArgs); + savedArgs = savedThis = null; + } + }, ms); + } + + return wrapper; +} \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/_js.view/test.js b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/_js.view/test.js new file mode 100644 index 00000000..5339c8d1 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/_js.view/test.js @@ -0,0 +1,47 @@ +describe("throttle(f, 1000)", function() { + let f1000; + let log = ""; + + function f(a) { + log += a; + } + + before(function() { + f1000 = throttle(f, 1000); + this.clock = sinon.useFakeTimers(); + }); + + it("the first call runs now", function() { + f1000(1); // runs now + assert.equal(log, "1"); + }); + + it("then calls are ignored till 1000ms when the last call works", function() { + f1000(2); // (throttling - less than 1000ms since the last run) + f1000(3); // (throttling - less than 1000ms since the last run) + // after 1000 ms f(3) call is scheduled + + assert.equal(log, "1"); // right now only the 1st call done + + this.clock.tick(1000); // after 1000ms... + assert.equal(log, "13"); // log==13, the call to f1000(3) is made + }); + + it("the third call waits 1000ms after the second call", function() { + this.clock.tick(100); + f1000(4); // (throttling - less than 1000ms since the last run) + this.clock.tick(100); + f1000(5); // (throttling - less than 1000ms since the last run) + this.clock.tick(700); + f1000(6); // (throttling - less than 1000ms since the last run) + + this.clock.tick(100); // now 100 + 100 + 700 + 100 = 1000ms passed + + assert.equal(log, "136"); // the last call was f(6) + }); + + after(function() { + this.clock.restore(); + }); + +}); \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/solution.md b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/solution.md new file mode 100644 index 00000000..372ebedd --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/solution.md @@ -0,0 +1,39 @@ +```js +function throttle(func, ms) { + + let isThrottled = false, + savedArgs, + savedThis; + + function wrapper() { + + if (isThrottled) { // (2) + savedArgs = arguments; + savedThis = this; + return; + } + + func.apply(this, arguments); // (1) + + isThrottled = true; + + setTimeout(function() { + isThrottled = false; // (3) + if (savedArgs) { + wrapper.apply(savedThis, savedArgs); + savedArgs = savedThis = null; + } + }, ms); + } + + return wrapper; +} +``` + +A call to `throttle(func, ms)` returns `wrapper`. + +1. During the first call, the `wrapper` just runs `func` and sets the cooldown state (`isThrottled = true`). +2. In this state all calls memorized in `savedArgs/savedThis`. Please note that both the context and the arguments are equally important and should be memorized. We need them simultaneously to reproduce the call. +3. ...Then after `ms` milliseconds pass, `setTimeout` triggers. The cooldown state is removed (`isThrottled = false`). And if we had ignored calls, then `wrapper` is executed with last memorized arguments and context. + +The 3rd step runs not `func`, but `wrapper`, because we not only need to execute `func`, but once again enter the cooldown state and setup the timeout to reset it. diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/task.md b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/task.md new file mode 100644 index 00000000..4af0d54c --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/task.md @@ -0,0 +1,48 @@ +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. + +**The difference with `debounce` -- if an ignored call is the last during the cooldown, then it executes at the end of the delay.** + +Let's check the real-life application to better understand that requirement and to see where it comes from. + +**For instance, we want to track mouse movements.** + +In browser we can setup a function to run at every mouse micro-movement and get the pointer location as it moves. During an active mouse usage, this function usually runs very frequently, can be something like 100 times per second (every 10 ms). + +**The tracking function should update some information on the web-page.** + +Updating function `update()` is too heavy to do it on every micro-movement. There is also no sense in making it more often than once per 100ms. + +So we'll assign `throttle(update, 100)` as the function to run on each mouse move instead of the original `update()`. The decorator will be called often, but `update()` will be called at maximum once per 100ms. + +Visually, it will look like this: + +1. For the first mouse movement the decorated variant passes the call to `update`. That's important, the user sees our reaction to his move immediately. +2. Then as the mouse moves on, until `100ms` nothing happens. The decorated variant ignores calls. +3. At the end of `100ms` -- one more `update` happens with the last coordinates. +4. Then, finally, the mouse stops somewhere. The decorated variant waits until `100ms` expire and then runs `update` runs with last coordinates. So, perhaps the most important, the final mouse coordinates are processed. + +A code example: + +```js +function f(a) { + console.log(a) +}; + +// f1000 passes calls to f at maximum once per 1000 ms +let f1000 = throttle(f, 1000); + +f1000(1); // shows 1 +f1000(2); // (throttling, 1000ms not out yet) +f1000(3); // (throttling, 1000ms not out yet) + +// when 1000 ms time out... +// ...outputs 3, intermediate value 2 was ignored +``` + +P.S. Arguments and the context `this` passed to `f1000` should be passed to the original `f`. 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 new file mode 100644 index 00000000..707bbf58 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/article.md @@ -0,0 +1,461 @@ +# Decorators and forwarding, call/apply + +JavaScript gives exceptional flexibility when dealing with functions. They can be passed around, used as objects, and now we'll see how to *forward* calls between them and *decorate* them. + +[cut] + +## Transparent caching + +Let's say we have a function `slow(x)` which is CPU-heavy, but its results are stable. In other words, for the same `x` it always returns the same result. + +If the function is called often, we may want to cache (remember) the results for different `x` to avoid spending extra-time on recalculations. + +But instead of adding that functionality into `slow()` we'll create a wrapper. As we'll see, there are many benefits of doing so. + +Here's the code, and explanations follow: + +```js run +function slow(x) { + // there can be a heavy CPU-intensive job here + alert(`Called with ${x}`); + return x; +} + +function cachingDecorator(func) { + let cache = new Map(); + + return function(x) { + if (cache.has(x)) { // if the result is in the map + return cache.get(x); // return it + } + + let result = func(x); // otherwise call func + + cache.set(x, result); // and cache (remember) the result + return result; + }; +} + +slow = cachingDecorator(slow); + +alert( slow(1) ); // slow(1) is cached +alert( "Again: " + slow(1) ); // the same + +alert( slow(2) ); // slow(2) is cached +alert( "Again: " + slow(2) ); // the same as the previous line +``` + +In the code above `cachingDecorator` is a *decorator*: a special function that takes another function and alters its behavior. + +The idea is that we can call `cachingDecorator` for any function, and it will return the caching wrapper. That's great, because we can have many functions that could use such a feature, and all we need to do is to apply `cachingDecorator` to them. + +By separating caching from the main function code we also keep the main code simpler. + +Now let's get into details of how it works. + +The result of `cachingDecorator(func)` is a "wrapper": `function(x)` that "wraps" the call of `func(x)` into caching logic: + +![](decorator-makecaching-wrapper.png) + +As we can see, the wrapper returns the result of `func(x)` "as is". From an outside code, the wrapped `slow` function still does the same. It just got a caching aspect added to its behavior. + +To summarize, there are several benefits of using a separate `cachingDecorator` instead of altering the code of `slow` itself: + +- The `cachingDecorator` is reusable. We can apply it to another function. +- The caching logic is separate, it did not increase the complexity of `slow` itself (if there were any). +- We can combine multiple decorators if needed (other decorators will follow). + + +## Using "func.call" for the context + +The caching decorator mentioned above is not suited to work with object methods. + +For instance, in the code below `user.format()` stops working after the decoration: + +```js run +// we'll make worker.slow caching +let worker = { + someMethod() { + return 1; + }, + + slow(x) { + // actually, there can be a scary CPU-heavy task here + alert("Called with " + x); + return x * this.someMethod(); // (*) + } +}; + +// same code as before +function cachingDecorator(func) { + let cache = new Map(); + return function(x) { + if (cache.has(x)) { + return cache.get(x); + } +*!* + let result = func(x); // (**) +*/!* + cache.set(x, result); + return result; + }; +} + +alert( worker.slow(1) ); // the original method works + +worker.slow = cachingDecorator(worker.slow); // now make it caching + +*!* +alert( worker.slow(2) ); // Whoops! Error: Cannot read property 'someMethod' of undefined +*/!* +``` + +The error occurs in the line `(*)` that tries to access `this.someMethod` and fails. Can you see why? + +The reason is that the wrapper calls the original function as `func(x)` in the line `(**)`. And, when called like that, the function gets `this = undefined`. + +We would observe a similar symptom if we tried to run: + +```js +let func = worker.slow; +func(2); +``` + +So, the wrapper passes the call to the original method, but without the context `this`. Hence the error. + +Let's fix it. + +There's a special built-in function method [func.call(context, ...args)](mdn:js/Function/call) that allows to call a function explicitly setting `this`. + +The syntax is: + +```js +func.call(context, arg1, arg2, ...) +``` + +It runs `func` providing the first argument as `this`, and the next as the arguments. + +To put it simple, these two calls do almost the same: +```js +func(1, 2, 3); +func.call(obj, 1, 2, 3) +``` + +They both call `func` with arguments `1`, `2` and `3`. The only difference is that `func.call` also sets `this` to `obj`. + +As an example, in the code below we call `sayHi` in the context of different objects: `sayHi.call(user)` runs `sayHi` providing `this=user`, and the next line sets `this=admin`: + +```js run +function sayHi() { + alert(this.name); +} + +let user = { name: "John" }; +let admin = { name: "Admin" }; + +// use call to pass different objects as "this" +sayHi.call( user ); // John +sayHi.call( admin ); // Admin +``` + +And here we use `call` to call `say` with the given context and phrase: + + +```js run +function say(phrase) { + alert(this.name + ': ' + phrase); +} + +let user = { name: "John" }; + +// user becomes this, and "Hello" becomes the first argument +say.call( user, "Hello" ); // John: Hello +``` + + +In our case, we can use `call` in the wrapper to pass the context to the original function: + + +```js run +let worker = { + someMethod() { + return 1; + }, + + slow(x) { + alert("Called with " + x); + return x * this.someMethod(); // (*) + } +}; + +function cachingDecorator(func) { + let cache = new Map(); + return function(x) { + if (cache.has(x)) { + return cache.get(x); + } +*!* + let result = func.call(this, x); // "this" is passed correctly now +*/!* + cache.set(x, result); + return result; + }; +} + +worker.slow = cachingDecorator(worker.slow); // now make it caching + +alert( worker.slow(2) ); // works +alert( worker.slow(2) ); // works, doesn't call the original (cached) +``` + +Now everything is fine. + +To make it all clear, let's see more deeply how `this` is passed along: + +1. After the decoration `worker.slow` is now the wrapper `function (x) { ... }`. +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" + +Now let's make `cachingDecorator` even more universal. Till now it was working only with single-argument functions. + +Now how to cache the multi-argument `worker.slow` method? + +```js +let worker = { + slow(min, max) { + return min + max; // scary CPU-hogger is assumed + } +}; + +// should remember same-argument calls +worker.slow = cachingDecorator(worker.slow); +``` + +We have two tasks to solve here. + +First is how to use both arguments `min` and `max` for the key in `cache` map. Previously, for a single argument `x` we could just `cache.set(x, result)` to save the result and `cache.get(x)` to retrieve it. But now we need to remember the result for a *combination of arguments* `(min,max)`. The native `Map` takes single value only as the key. + +There are many solutions possible: + +1. Implement a new (or use a third-party) map-like data structure that is more versatile and allows multi-keys. +2. Use nested maps: `cache.set(min)` will be a `Map` that stores the pair `(max, result)`. So we can get `result` as `cache.get(min).get(max)`. +3. Join two values into one. In our particular case we can just use a string `"min,max"` as the `Map` key. For flexibility, we can allow to provide a *hashing function* for the decorator, that knows how to make a one value from many. + + +For many practical applications, the 3rd variant is good enough, so we'll stick to it. + +The second task to solve is how to pass many arguments to `func`. Currently, the wrapper `function(x)` assumes a single argument, and `func.call(this, x)` passes it. + +Here we can use another built-in method [func.apply](mdn:js/Function/apply). + +The syntax is: + +```js +func.apply(context, args) +``` + +It runs the `func` setting `this=context` and using an array-like object `args` as the list of arguments. + + +For instance, these two calls are almost the same: + +```js +func(1, 2, 3); +func.apply(context, [1, 2, 3]) +``` + +Both run `func` giving it arguments `1,2,3`. But `apply` also sets `this=context`. + +For instance, here `say` is called with `this=user` and `messageData` as a list of arguments: + +```js run +function say(time, phrase) { + alert(`[${time}] ${this.name}: ${phrase}`); +} + +let user = { name: "John" }; + +let messageData = ['10:00', 'Hello']; // become time and phrase + +*!* +// user becomes this, messageData is passed as a list of arguments (time, phrase) +say.apply(user, messageData); // [10:00] John: Hello (this=user) +*/!* +``` + +The only syntax difference between `call` and `apply` is that `call` expects a list of arguments, while `apply` takes an array-like object with them. + +We already know the spread operator `...` from the chapter that can pass an array (or any iterable) as a list of arguments. So if we use it with `call`, we can achieve almost the same as `apply`. + +These two calls are almost equivalent: + +```js +let args = [1, 2, 3]; + +*!* +func.call(context, ...args); // pass an array as list with spread operator +func.apply(context, args); // is same as using apply +*/!* +``` + +If we look more closely, there's a minor difference between such uses of `call` and `apply`. + +- The spread operator `...` allows to pass *iterable* `args` as the list to `call`. +- The `apply` accepts only *array-like* `args`. + +So, these calls complement each other. Where we expect an iterable, `call` works, where we expect an array-like, `apply` works. + +And if `args` is both iterable and array-like, like a real array, then we technically could use any of them, but `apply` will probably be faster, because it's a single operation. Most JavaScript engines internally optimize is better than a pair `call + spread`. + +One of the most important uses of `apply` is passing the call to another function, like this: + +```js +let wrapper = function() { + return anotherFunction.apply(this, arguments); +}; +``` + +That's called *call forwarding*. The `wrapper` passes everything it gets: the context `this` and arguments to `anotherFunction` and returns back its result. + +When an external code calls such `wrapper`, it is undistinguishable from the call of the original function. + +Now let's bake it all into the more powerful `cachingDecorator`: + +```js run +let worker = { + slow(min, max) { + alert(`Called with ${min},${max}`); + return min + max; + } +}; + +function cachingDecorator(func, hash) { + let cache = new Map(); + return function() { +*!* + let key = hash(arguments); // (*) +*/!* + if (cache.has(key)) { + return cache.get(key); + } + +*!* + let result = func.apply(this, arguments); // (**) +*/!* + + cache.set(key, result); + return result; + }; +} + +function hash(args) { + return args[0] + ',' + args[1]; +} + +worker.slow = cachingDecorator(worker.slow, hash); + +alert( worker.slow(3, 5) ); // works +alert( "Again " + worker.slow(3, 5) ); // same (cached) +``` + +Now the wrapper operates with any number of arguments. + +There are two changes: + +- In the line `(*)` it calls `hash` to create a single key from `arguments`. Here we use a simple "joining" function that turns arguments `(3, 5)` into the key `"3,5"`. More complex cases may require other hashing functions. +- Then `(**)` uses `func.apply` to pass both the context and all arguments the wrapper got (no matter how many) to the original function. + + +## Borrowing a method [#method-borrowing] + +Now let's make one more minor improvement in the hashing function: + +```js +function hash(args) { + return args[0] + ',' + args[1]; +} +``` + +As of now, it works only on two arguments. It would be better if it could glue any number of `args`. + +The natural solution would be to use [arr.join](mdn:js/Array/join) method: + +```js +function hash(args) { + return args.join(); +} +``` + +...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: + +```js run +function hash() { +*!* + alert( arguments.join() ); // Error: arguments.join is not a function +*/!* +} + +hash(1, 2); +``` + +Still, there's an easy way to use array join: + +```js run +function hash() { +*!* + alert( [].join.call(arguments) ); // 1,2 +*/!* +} + +hash(1, 2); +``` + +The trick is called *method borrowing*. + +We take (borrow) a join method from a regular array `[].join`. And use `[].join.call` to run it in the context of `arguments`. + +Why does it work? + +That's because the internal algorithm of the native method `arr.join(glue)` is very simple. + +Taken from the specification almost "as-is": + +1. Let `glue` be the first argument or, if no arguments, then a comma `","`. +2. Let `result` be an empty string. +3. Append `this[0]` to `result`. +4. Append `glue` and `this[1]`. +5. Append `glue` and `this[2]`. +6. ...Do so until `this.length` items are glued. +7. Return `result`. + +So, technically it takes `this` and joins `this[0]`, `this[1]` ...etc together. It's intentionally written in a way that allows any array-like `this` (not a coincidence, many methods follow this practice). That's why it also works with `this=arguments`. + +## Summary + +*Decorator* is a wrapper around a function that alters its behavior. The main job is still carried out by the function. + +It is generally safe to replace a function or a method with a decorated one, except for one little thing. If the original function had properties on it, like `func.calledCount` or whatever, then the decorated one will not provide them. Because that is a wrapper. So one need to be careful if one uses them. Some decorators provide their own properties. + +Decorators can be seen as "features" or "aspects" that can be added to a function. We can add one or add many. And all this without changing its code! + +To implement `cachingDecorator`, we studied methods: + +- [func.call(context, arg1, arg2...)](mdn:js/Function/call) -- calls `func` with given context and arguments. +- [func.apply(context, args)](mdn:js/Function/apply) -- calls `func` passing `context` as `this` and array-like `args` into a list of arguments. + +The generic *call forwarding* is usually done with `apply`: + +```js +let wrapper = function() { + return original.apply(this, arguments); +} +``` + +We also saw an example of *method borrowing* when we take a method from an object and `call` it in the context of another object. It is quite common to take array methods and apply them to arguments. The alternative is to use rest parameters object that is a real array. + + +There are many decorators there in the wild. Check how well you got them by solving the tasks of this chapter. diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.png b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.png new file mode 100644 index 00000000..171e2791 Binary files /dev/null and b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.png differ diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper@2x.png b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper@2x.png new file mode 100644 index 00000000..8d3b5434 Binary files /dev/null and b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper@2x.png differ diff --git a/1-js/06-advanced-functions/10-bind/2-write-to-object-after-bind/solution.md b/1-js/06-advanced-functions/10-bind/2-write-to-object-after-bind/solution.md new file mode 100644 index 00000000..737a1448 --- /dev/null +++ b/1-js/06-advanced-functions/10-bind/2-write-to-object-after-bind/solution.md @@ -0,0 +1,18 @@ +The answer: `null`. + + +```js run +function f() { + alert( this ); // null +} + +let user = { + g: f.bind(null) +}; + +user.g(); +``` + +The context of a bound function is hard-fixed. There's just no way to further change it. + +So even while we run `user.g()`, the original function is called with `this=null`. diff --git a/1-js/06-advanced-functions/10-bind/2-write-to-object-after-bind/task.md b/1-js/06-advanced-functions/10-bind/2-write-to-object-after-bind/task.md new file mode 100644 index 00000000..6d7e1fb2 --- /dev/null +++ b/1-js/06-advanced-functions/10-bind/2-write-to-object-after-bind/task.md @@ -0,0 +1,20 @@ +importance: 5 + +--- + +# Bound function as a method + +What will be the output? + +```js +function f() { + alert( this ); // ? +} + +let user = { + g: f.bind(null) +}; + +user.g(); +``` + diff --git a/1-js/06-advanced-functions/10-bind/3-second-bind/solution.md b/1-js/06-advanced-functions/10-bind/3-second-bind/solution.md new file mode 100644 index 00000000..97e1c280 --- /dev/null +++ b/1-js/06-advanced-functions/10-bind/3-second-bind/solution.md @@ -0,0 +1,15 @@ +The answer: **John**. + +```js run no-beautify +function f() { + alert(this.name); +} + +f = f.bind( {name: "John"} ).bind( {name: "Pete"} ); + +f(); // John +``` + +The exotic [bound function](https://tc39.github.io/ecma262/#sec-bound-function-exotic-objects) object returned by `f.bind(...)` remembers the context (and arguments if provided) only at creation time. + +A function cannot be re-bound. diff --git a/1-js/06-advanced-functions/10-bind/3-second-bind/task.md b/1-js/06-advanced-functions/10-bind/3-second-bind/task.md new file mode 100644 index 00000000..5daf053c --- /dev/null +++ b/1-js/06-advanced-functions/10-bind/3-second-bind/task.md @@ -0,0 +1,20 @@ +importance: 5 + +--- + +# Second bind + +Can we change `this` by additional binding? + +What will be the output? + +```js no-beautify +function f() { + alert(this.name); +} + +f = f.bind( {name: "John"} ).bind( {name: "Ann" } ); + +f(); +``` + diff --git a/1-js/06-advanced-functions/10-bind/4-function-property-after-bind/solution.md b/1-js/06-advanced-functions/10-bind/4-function-property-after-bind/solution.md new file mode 100644 index 00000000..181555d9 --- /dev/null +++ b/1-js/06-advanced-functions/10-bind/4-function-property-after-bind/solution.md @@ -0,0 +1,4 @@ +The answer: `undefined`. + +The result of `bind` is another object. It does not have the `test` property. + diff --git a/1-js/06-advanced-functions/10-bind/4-function-property-after-bind/task.md b/1-js/06-advanced-functions/10-bind/4-function-property-after-bind/task.md new file mode 100644 index 00000000..8cd18ec5 --- /dev/null +++ b/1-js/06-advanced-functions/10-bind/4-function-property-after-bind/task.md @@ -0,0 +1,23 @@ +importance: 5 + +--- + +# Function property after bind + +There's a value in the property of a function. Will it change after `bind`? Why, elaborate? + +```js run +function sayHi() { + alert( this.name ); +} +sayHi.test = 5; + +*!* +let bound = sayHi.bind({ + name: "John" +}); + +alert( bound.test ); // what will be the output? why? +*/!* +``` + diff --git a/1-js/06-advanced-functions/10-bind/5-question-use-bind/solution.md b/1-js/06-advanced-functions/10-bind/5-question-use-bind/solution.md new file mode 100644 index 00000000..0cb673b1 --- /dev/null +++ b/1-js/06-advanced-functions/10-bind/5-question-use-bind/solution.md @@ -0,0 +1,43 @@ + +The error occurs because `ask` gets functions `loginOk/loginFail` without the object. + +When it calls them, they naturally assume `this=undefined`. + +Let's `bind` the context: + +```js run +function askPassword(ok, fail) { + let password = prompt("Password?", ''); + if (password == "rockstar") ok(); + else fail(); +} + +let user = { + name: 'John', + + loginOk() { + alert(`${this.name} logged in`); + }, + + loginFail() { + alert(`${this.name} failed to log in`); + }, + +}; + +*!* +askPassword(user.loginOk.bind(user), user.loginFail.bind(user)); +*/!* +``` + +Now it works. + +An alternative solution could be: +```js +//... +askPassword(() => user.loginOk(), () => user.loginFail()); +``` + +Usually that also works, but may fail in more complex situations where `user` has a chance of being overwritten between the moments of asking and running `() => user.loginOk()`. + + diff --git a/1-js/06-advanced-functions/10-bind/5-question-use-bind/task.md b/1-js/06-advanced-functions/10-bind/5-question-use-bind/task.md new file mode 100644 index 00000000..eb19e664 --- /dev/null +++ b/1-js/06-advanced-functions/10-bind/5-question-use-bind/task.md @@ -0,0 +1,38 @@ +importance: 5 + +--- + +# Ask losing this + +The call to `askPassword()` in the code below should check the password and then call `user.loginOk/loginFail` depending on the answer. + +But it leads to an error. Why? + +Fix the highlighted line for everything to start working right (other lines are not to be changed). + +```js run +function askPassword(ok, fail) { + let password = prompt("Password?", ''); + if (password == "rockstar") ok(); + else fail(); +} + +let user = { + name: 'John', + + loginOk() { + alert(`${this.name} logged in`); + }, + + loginFail() { + alert(`${this.name} failed to log in`); + }, + +}; + +*!* +askPassword(user.loginOk, user.loginFail); +*/!* +``` + + diff --git a/1-js/06-advanced-functions/10-bind/article.md b/1-js/06-advanced-functions/10-bind/article.md new file mode 100644 index 00000000..87637929 --- /dev/null +++ b/1-js/06-advanced-functions/10-bind/article.md @@ -0,0 +1,205 @@ +libs: + - lodash + +--- + +# Function binding + +When using `setTimeout` with object methods or passing object methods along, there's a known problem: "losing `this`". + +Suddenly, `this` just stops working right. The situation is typical for novice developers, but happens with experienced ones as well. + +[cut] + +## Losing "this" + +We already know that in JavaScript it's easy to lose `this`. Once a method is passed somewhere separately from the object -- `this` is lost. + +Here's how it may happen with `setTimeout`: + +```js run +let user = { + firstName: "John", + sayHi() { + alert(`Hello, ${this.firstName}!`); + } +}; + +*!* +setTimeout(user.sayHi, 1000); // Hello, undefined! +*/!* +``` + +As we can see, the output shows not "John" as `this.firstName`, but `undefined`! + +That's because `setTimeout` got the function `user.sayHi`, separately from the object. The last line can be rewritten as: + +```js +let f = user.sayHi; +setTimeout(f, 1000); // lost user context +``` + +The method `setTimeout` in-browser is a little special: it sets `this=window` for the function call (for Node.JS, `this` becomes the timer object, but doesn't really matter here). So for `this.firstName` it tries to get `window.firstName`, which does not exist. In other similar cases as we'll see, usually `this` just becomes `undefined`. + +The task is quite typical -- we want to pass an object method somewhere else (here -- to the scheduler) where it will be called. How to make sure that it will be called in the right context? + +## Solution 1: a wrapper + +The simplest solution is to use an wrapping function: + +```js run +let user = { + firstName: "John", + sayHi() { + alert(`Hello, ${this.firstName}!`); + } +}; + +*!* +setTimeout(function() { + user.sayHi(); // Hello, John! +}, 1000); +*/!* +``` + +Now it works, because it receives `user` from the outer lexical environment, and then calls the method normally. + +The same, but shorter: + +```js +setTimeout(() => user.sayHi(), 1000); // Hello, John! +``` + +Looks fine, but a slight vulnerability appears in our code structure. + +What if before `setTimeout` triggers (there's one second delay!) `user` changes value? Then, suddenly, the it will call the wrong object! + + +```js run +let user = { + firstName: "John", + sayHi() { + alert(`Hello, ${this.firstName}!`); + } +}; + +setTimeout(() => user.sayHi(), 1000); + +// ...within 1 second +user = { sayHi() { alert("Another user in setTimeout!"); } }; + +// Another user in setTimeout?!? +``` + +The next solution guarantees that such thing won't happen. + +## Solution 2: bind + +Functions provide a built-in method [bind](mdn:js/Function/bind) that allows to fix `this`. + +The basic syntax is: + +```js +// more complex syntax will be little later +let boundFunc = func.bind(context); +```` + +The result of `func.bind(context)` is a special function-like "exotic object", that is callable as function and transparently passes the call to `func` setting `this=context`. + +In other words, calling `boundFunc` is like `func` with fixed `this`. + +For instance, here `funcUser` passes a call to `func` with `this=user`: + +```js run +let user = { + firstName: "John" +}; + +function func() { + alert(this.firstName); +} + +*!* +let funcUser = func.bind(user); +funcUser(); // John +*/!* +``` + +Here `func.bind(user)` as a "bound variant" of `func`, with fixed `this=user`. + +All arguments are passed to the original `func` "as is", for instance: + +```js run +let user = { + firstName: "John" +}; + +function func(phrase) { + alert(phrase + ', ' + this.firstName); +} + +// bind this to user +let funcUser = func.bind(user); + +*!* +funcUser("Hello"); // Hello, John (argument "Hello" is passed, and this=user) +*/!* +``` + +Now let's try with an object method: + + +```js run +let user = { + firstName: "John", + sayHi() { + alert(`Hello, ${this.firstName}!`); + } +}; + +*!* +let sayHi = user.sayHi.bind(user); // (*) +*/!* + +sayHi(); // Hello, John! + +setTimeout(sayHi, 1000); // Hello, John! +``` + +In the line `(*)` we take the method `user.sayHi` and bind it to `user`. The `sayHi` is a "bound" function, that can be called alone or passed to `setTimeout` -- doesn't matter, the context will be right. + +Here we can see that arguments are passed "as is", only `this` is fixed by `bind`: + +```js run +let user = { + firstName: "John", + say(phrase) { + alert(`${phrase}, ${this.firstName}!`); + } +}; + +let say = user.say.bind(user); + +say("Hello"); // Hello, John ("Hello" argument is passed to say) +say("Bye"); // Bye, John ("Bye" is passed to say) +``` + +````smart header="Convenience method: `bindAll`" +If an object has many methods and we plan to actively pass it around, then we could bind them all in a loop: + +```js +for (let key in user) { + if (typeof user[key] == 'function') { + user[key] = user[key].bind(user); + } +} +``` + +JavaScript libraries also provide functions for convenient mass binding , e.g. [_.bindAll(obj)](http://lodash.com/docs#bindAll) in lodash. +```` + +## Summary + +Method `func.bind(context, ...args)` returns a "bound variant" of function `func` that fixes the context `this` and first arguments if given. + +Usually we apply `bind` to fix `this` in an object method, so that we can pass it somewhere. For example, to `setTimeout`. There are more reasons to `bind` in the modern development, we'll meet them later. diff --git a/1-js/06-advanced-functions/10-bind/head.html b/1-js/06-advanced-functions/10-bind/head.html new file mode 100644 index 00000000..afdb888d --- /dev/null +++ b/1-js/06-advanced-functions/10-bind/head.html @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/1-js/06-advanced-functions/11-currying-partials/1-ask-currying/solution.md b/1-js/06-advanced-functions/11-currying-partials/1-ask-currying/solution.md new file mode 100644 index 00000000..3284c943 --- /dev/null +++ b/1-js/06-advanced-functions/11-currying-partials/1-ask-currying/solution.md @@ -0,0 +1,16 @@ + + +1. Either use a wrapper function, an arrow to be concise: + + ```js + askPassword(() => user.login(true), () => user.login(false)); + ``` + + Now it gets `user` from outer variables and runs it the normal way. + +2. Or create a partial function from `user.login` that uses `user` as the context and has the correct first argument: + + + ```js + askPassword(user.login.bind(user, true), user.login.bind(user, false)); + ``` diff --git a/1-js/06-advanced-functions/11-currying-partials/1-ask-currying/task.md b/1-js/06-advanced-functions/11-currying-partials/1-ask-currying/task.md new file mode 100644 index 00000000..915d8aee --- /dev/null +++ b/1-js/06-advanced-functions/11-currying-partials/1-ask-currying/task.md @@ -0,0 +1,34 @@ +importance: 5 + +--- + +# Partial application for login + +The task is a little more complex variant of . + +The `user` object was modified. Now instead of two functions `loginOk/loginFail`, it has a single function `user.login(true/false)`. + +What to pass `askPassword` in the code below, so that it calls `user.login(true)` as `ok` and `user.login(fail)` as `fail`? + +```js +function askPassword(ok, fail) { + let password = prompt("Password?", ''); + if (password == "rockstar") ok(); + else fail(); +} + +let user = { + name: 'John', + + login(result) { + alert( this.name + (result ? ' logged in' : ' failed to log in') ); + } +}; + +*!* +askPassword(?, ?); // ? +*/!* +``` + +Your changes should only modify the highlighted fragment. + diff --git a/1-js/06-advanced-functions/11-currying-partials/article.md b/1-js/06-advanced-functions/11-currying-partials/article.md new file mode 100644 index 00000000..6e7b78e8 --- /dev/null +++ b/1-js/06-advanced-functions/11-currying-partials/article.md @@ -0,0 +1,298 @@ +libs: + - lodash + +--- + +# Currying and partials + +Till now we were only talking about binding `this`. Now let's make a step further. + +We can bind not only `this`, but also arguments. That's rarely done, but sometimes can be handy. + +[cut] + +The full syntax of `bind`: + +```js +let bound = func.bind(context, arg1, arg2, ...); +``` + +It allows to bind context as `this` and starting arguments of the function. + +For instance, we have a multiplication function `mul(a, b)`: + +```js +function mul(a, b) { + return a * b; +} +``` + +Let's use `bind` to create a function `double` on its base: + +```js run +*!* +let double = mul.bind(null, 2); +*/!* + +alert( double(3) ); // = mul(2, 3) = 6 +alert( double(4) ); // = mul(2, 4) = 8 +alert( double(5) ); // = mul(2, 5) = 10 +``` + +The call to `mul.bind(null, 2)` creates a new function `double` that passes calls to `mul`, fixing `null` as the context and `2` as the first argument. Further arguments are passed "as is". + +That's called [partial function application](https://en.wikipedia.org/wiki/Partial_application) -- we create a new function by fixing some parameters of the existing one. + +Please note that here we actually don't use `this` here. But `bind` requires it, so we must put in something like `null`. + +The function `triple` in the code below triples the value: + +```js run +*!* +let triple = mul.bind(null, 3); +*/!* + +alert( triple(3) ); // = mul(3, 3) = 9 +alert( triple(4) ); // = mul(3, 4) = 12 +alert( triple(5) ); // = mul(3, 5) = 15 +``` + +Why do we usually make a partial function? + +Here our benefit is that we created an independent function with a readable name (`double`, `triple`). We can use it and don't write the first argument of every time, cause it's fixed with `bind`. + +In other cases, partial application is useful when we have a very generic function, and want a less universal variant of it for convenience. + +For instance, we have a function `send(from, to, text)`. Then, inside a `user` object we may want to use a partial variant of it: `sendTo(to, text)` that sends from the current user. + +## Going partial without context + +What if we'd like to fix some arguments, but not bind `this`? + +The native `bind` does not allow that. We can't just omit the context and jump to arguments. + +Fortunately, a `partial` function for binding only arguments can be easily implemented. + +Like this: + +```js run +*!* +function partial(func, ...argsBound) { + return function(...args) { // (*) + return func.call(this, ...argsBound, ...args); + } +} +*/!* + +// Usage: +let user = { + firstName: "John", + say(time, phrase) { + alert(`[${time}] ${this.firstName}: ${phrase}!`); + } +}; + +// add a partial method that says something now by fixing the first argument +user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); + +user.sayNow("Hello"); +// Something like: +// [10:00] Hello, John! +``` + +The result of `partial(func[, arg1, arg2...])` call is a wrapper `(*)` that calls `func` with: +- Same `this` as it gets (for `user.sayNow` call it's `user`) +- Then gives it `...argsBound` -- arguments from the `partial` call (`"10:00"`) +- Then gives it `...args` -- arguments given to the wrapper (`"Hello"`) + +So easy to do it with the spread operator, right? + +Also there's a ready [_.partial](https://lodash.com/docs#partial) implementation from lodash library. + +## Currying + +Sometimes people mix up partial function application mentioned above with another thing named "currying". That's another interesting technique of working with functions that we just have to mention here. + +[Currying](https://en.wikipedia.org/wiki/Currying) is translating a function from callable as `f(a, b, c)` into callable as `f(a)(b)(c)`. + +Let's make `curry` function that performs currying for binary functions. In other words, it translates `f(a, b)` into `f(a)(b)`: + +```js run +*!* +function curry(func) { + return function(a) { + return function(b) { + return func(a, b); + }; + }; +} +*/!* + +// usage +function sum(a, b) { + return a + b; +} + +let carriedSum = curry(sum); + +alert( carriedSum(1)(2) ); // 3 +``` + +As you can see, the implementation is a series of wrappers. + +- The result of `curry(func)` is a wrapper `function(a)`. +- When it is called like `sum(1)`, the argument is saved in the Lexical Environment, and a new wrapper is returned `function(b)`. +- Then `sum(1)(2)` finally calls `function(b)` providing `2`, and it passes the call to the original multi-argument `sum`. + +More advanced implementations of currying like [_.curry](https://lodash.com/docs#curry) from lodash library do something more sophisticated. They return a wrapper that allows a function to be called normally when all arguments are supplied *or* returns a partial otherwise. + +```js +function curry(f) { + return function(..args) { + // if args.length == f.length (as many arguments as f has), + // then pass the call to f + // otherwise return a partial function that fixes args as first arguments + }; +} +``` + +## Currying? What for? + +Advanced currying allows both to keep the function callable normally and to get partials easily. To understand the benefits we definitely need a worthy real-life example. + +For instance, we have the logging function `log(date, importance, message)` that formats and outputs the information. In real projects such functions also have many other useful features like: sending it over the network or filtering: + +```js +function log(date, importance, message) { + alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`); +} +``` + +Let's curry it! + +```js +log = _.curry(log); +``` + +After that `log` still works the normal way: + +```js +log(new Date(), "DEBUG", "some debug"); +``` + +...But also can be called in the curried form: + +```js +log(new Date())("DEBUG")("some debug"); // log(a)(b)(c) +``` + +Let's get a convenience function for today's logs: + +```js +// todayLog will be the partial of log with fixed first argument +let todayLog = log(new Date()); + +// use it +todayLog("INFO", "message"); // [HH:mm] INFO message +``` + +And now a convenience function for today's debug messages: + +```js +let todayDebug = todayLog("DEBUG"); + +todayDebug("message"); // [HH:mm] DEBUG message +``` + +So: +1. We didn't lose anything after currying: `log` is still callable normally. +2. We were able to generate partial functions that are convenient in many cases. + +## Advanced curry implementation + +In case you're interested, here's the "advanced" curry implementation that we could use above. + +```js run +function curry(func) { + + return function curried(...args) { + if (args.length >= func.length) { + return func.apply(this, args); + } else { + return function(...args2) { + return curried.apply(this, args.concat(args2)); + } + } + }; + +} + +function sum(a, b, c) { + return a + b + c; +} + +let curriedSum = curry(sum); + +// still callable normally +alert( curriedSum(1, 2, 3) ); // 6 + +// get the partial with curried(1) and call it with 2 other arguments +alert( curriedSum(1)(2,3) ); // 6 + +// full curried form +alert( curriedSum(1)(2)(3) ); // 6 +``` + +The new `curry` may look complicated, but it's actually pretty easy to understand. + +The result of `curry(func)` is the wrapper `curried` that looks like this: + +```js +// func is the function to transform +function curried(...args) { + if (args.length >= func.length) { // (1) + return func.apply(this, args); + } else { + return function pass(...args2) { // (2) + return curried.apply(this, args.concat(args2)); + } + } +}; +``` + +When we run it, there are two branches: + +1. Call now: if passed `args` count is the same as the original function has in its definition (`func.length`) or longer, then just pass the call to it. +2. Get a partial: otherwise, `func` is not called yet. Instead, another wrapper `pass` is returned, that will re-apply `curried` providing previous arguments together with the new ones. Then on a new call, again, we'll get either a new partial (if not enough arguments) or, finally, the result. + +For instance, let's see what happens in the case of `sum(a, b, c)`. Three arguments, so `sum.length = 3`. + +For the call `curried(1)(2)(3)`: + +1. The first call `curried(1)` remembers `1` in its Lexical Environment, and returns a wrapper `pass`. +2. The wrapper `pass` is called with `(2)`: it takes previous args (`1`), concatenates them with what it got `(2)` and calls `curried(1, 2)` with them together. + + As the argument count is still less than 3, `curry` returns `pass`. +3. The wrapper `pass` is called again with `(3)`, for the next call `pass(3)` takes previous args (`1`, `2`) and adds `3` to them, making the call `curried(1, 2, 3)` -- there are `3` arguments at last, they are given to the original function. + +If that's still not obvious, just trace the calls sequence in your mind or on the paper. + +```smart header="Fixed-length functions only" +The currying requires the function to have a known fixed number of arguments. +``` + +```smart header="A little more than currying" +By definition, currying should convert `sum(a, b, c)` into `sum(a)(b)(c)`. + +But most implementations of currying in JavaScript are advanced, as described: they also keep the function callable in the multi-argument variant. +``` + +## Summary + +- When we fix some arguments of an existing function, the resulting (less universal) function is called *a partial*. We can use `bind` to get a partial, but there are other ways also. + + Partials are convenient when we don't want to repeat the same argument over and over again. Like if we have a `send(from, to)` function, and `from` should always be the same for our task, we can get a partial and go on with it. + +- *Currying* is a transform that makes `f(a,b,c)` callable as `f(a)(b)(c)`. JavaScript implementations usually both keep the function callable normally and return the partial if arguments count is not enough. + + Currying is great when we want easy partials. As we've seen in the logging example: the universal function `log(date, importance, message)` after currying gives us partials when called with one argument like `log(date)` or two arguments `log(date, importance)`. diff --git a/1-js/06-advanced-functions/12-arrow-functions/article.md b/1-js/06-advanced-functions/12-arrow-functions/article.md new file mode 100644 index 00000000..dcae22a4 --- /dev/null +++ b/1-js/06-advanced-functions/12-arrow-functions/article.md @@ -0,0 +1,128 @@ +# Arrow functions revisited + +Let's revisit arrow functions. + +[cut] + +Arrow functions are not just a "shorthand" for writing small stuff. + +JavaScript is full of situations where we need to write a small function, that's executed somewhere else. + +For instance: + +- `arr.forEach(func)` -- `func` is executed by `forEach` for every array item. +- `setTimeout(func)` -- `func` is executed by the built-in scheduler. +- ...there are more. + +It's in the very spirit of JavaScript to create a function and pass it somewhere. + +And in such functions we usually don't want to leave the current context. + +## Arrow functions have no "this" + +As we remember from the chapter , arrow functions do not have `this`. If `this` is accessed, it is taken from the outside. + +For instance, we can use it to iterate inside an object method: + +```js run +let group = { + title: "Our Group", + students: ["John", "Pete", "Alice"], + + showList() { +*!* + this.students.forEach( + student => alert(this.title + ': ' + student) + ); +*/!* + } +}; + +group.showList(); +``` + +Here in `forEach`, the arrow function is used, so `this.title` in it is exactly the same as in the outer method `showList`. That is: `group.title`. + +If we used a "regular" function, there would be an error: + +```js run +let group = { + title: "Our Group", + students: ["John", "Pete", "Alice"], + + showList() { +*!* + this.students.forEach(function(student) { + // Error: Cannot read property 'title' of undefined + alert(this.title + ': ' + student) + }); +*/!* + } +}; + +group.showList(); +``` + +The error occurs because `forEach` runs functions with `this=undefined` by default, so the attempt to access `undefined.title` is made. + +That doesn't affect arrow functions, because they just don't have `this`. + +```warn header="Arrow functions can't run with `new`" +Not having `this` naturally means another limitation: arrow functions can't be used as constructors. They can't be called with `new`. +``` + +```smart header="Arrow functions VS bind" +There's a subtle difference between an arrow function `=>` and a regular function called with `.bind(this)`: + +- `.bind(this)` creates a "bound version" of the function. +- The arrow `=>` doesn't create any binding. The function simply doesn't have `this`. The lookup of `this` is made exactly the same way as a regular variable search: in the outer lexical environment. +``` + +## Arrows have no "arguments" + +Arrow functions also have no `arguments` variable. + +That's great for decorators, when we need to forward a call with the current `this` and `arguments`. + +For instance, `defer(f, ms)` gets a function and returns a wrapper around it that delays the call by `ms` milliseconds: + +```js run +function defer(f, ms) { + return function() { + setTimeout(() => f.apply(this, arguments), ms) + }; +} + +function sayHi(who) { + alert('Hello, ' + who); +} + +let sayHiDeferred = defer(sayHi, 2000); +sayHiDeferred("John"); // Hello, John after 2 seconds +``` + +The same without an arrow function would look like: + +```js +function defer(f, ms) { + return function(...args) { + let ctx = this; + setTimeout(function() { + return f.apply(ctx, args); + }, ms); + }; +} +``` + +Here we had to create additional variables `args` and `ctx` so that the function inside `setTimeout` could take them. + +## Summary + +Arrow functions: + +- Do not have `this`. +- Do not have `arguments`. +- Can't be called with `new`. +- (They also don't have `super`, but we didn't study it. Will be in the chapter ). + +That's because they are meant for short pieces of code that does not have their own "context", but rather works in the current one. And they really shine in that use case. diff --git a/1-js/06-advanced-functions/index.md b/1-js/06-advanced-functions/index.md new file mode 100644 index 00000000..7800dcc4 --- /dev/null +++ b/1-js/06-advanced-functions/index.md @@ -0,0 +1 @@ +# Advanced working with functions diff --git a/1-js/07-object-oriented-programming/01-property-descriptors/article.md b/1-js/07-object-oriented-programming/01-property-descriptors/article.md new file mode 100644 index 00000000..92d83365 --- /dev/null +++ b/1-js/07-object-oriented-programming/01-property-descriptors/article.md @@ -0,0 +1,311 @@ + +# Property flags and descriptors + +As we know, objects can store properties. + +Till now, a property was a simple "key-value" pair to us. But an object property is actually more complex and tunable thing. + +[cut] + +## Property flags + +Object properties, besides a **`value`**, have three special attributes (so-called "flags"): + +- **`writable`** -- if `true`, can be changed, otherwise it's read-only. +- **`enumerable`** -- if `true`, then listed in loops, otherwise not listed. +- **`configurable`** -- if `true`, the property can be deleted and these attributes can be modified, otherwise not. + +We didn't see them yet, because generally they do not show up. When we create a property "the usual way", all of them are `true`. But we also can change them any time. + +First, let's see how to get those flags. + +The method [Object.getOwnPropertyDescriptor](mdn:js/Object/getOwnPropertyDescriptor) allows to query the *full* information about a property. + +The syntax is: +```js +let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName); +``` + +`obj` +: The object to get information from. + +`propertyName` +: The name of the property. + +The returned value is a so-called "property descriptor" object: it contains the value and all the flags. + +For instance: + +```js run +let user = { + name: "John" +}; + +let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); + +alert( JSON.stringify(descriptor, null, 2 ) ); +/* property descriptor: +{ + "value": "John", + "writable": true, + "enumerable": true, + "configurable": true +} +*/ +``` + +To change the flags, we can use [Object.defineProperty](mdn:js/Object/defineProperty). + +The syntax is: + +```js +Object.defineProperty(obj, propertyName, descriptor) +``` + +`obj`, `propertyName` +: The object and property to work on. + +`descriptor` +: Property descriptor to apply. + +If the property exists, `defineProperty` updates its flags. Otherwise, it creates the property with the given value and flags; in that case, if a flag is not supplied, it is assumed `false`. + +For instance, here a property `name` is created with all falsy flags: + +```js run +let user = {}; + +*!* +Object.defineProperty(user, "name", { + value: "John" +}); +*/!* + +let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); + +alert( JSON.stringify(descriptor, null, 2 ) ); +/* +{ + "value": "John", +*!* + "writable": false, + "enumerable": false, + "configurable": false +*/!* +} + */ +``` + +Compare it with "normally created" `user.name` above: now all flags are falsy. If that's not what we want then we'd better set them to `true` in `descriptor`. + +Now let's see effects of the flags by example. + +## Read-only + +Let's make `user.name` read-only by changing `writable` flag: + +```js run +let user = { + name: "John" +}; + +Object.defineProperty(user, "name", { +*!* + writable: false +*/!* +}); + +*!* +user.name = "Pete"; // Error: Cannot assign to read only property 'name'... +*/!* +``` + +Now no one can change the name of our user, unless he applies his own `defineProperty` to override ours. + +Here's the same operation, but for the case when a property doesn't exist: + +```js run +let user = { }; + +Object.defineProperty(user, "name", { +*!* + value: "Pete", + // for new properties need to explicitly list what's true + enumerable: true, + configurable: true +*/!* +}); + +alert(user.name); // Pete +user.name = "Alice"; // Error +``` + + +## Non-enumerable + +Now let's a custom `toString` to `user`. + +Normally, a built-in `toString` for objects is non-enumerable, it does not show up in `for..in`. But if we add `toString` of our own, then by default it shows up in `for..in`. + +...But if we don't like it, then we can set `enumerable:false`. Then it won't appear in `for..in` loop, just like the built-in one: + +```js run +let user = { + name: "John", + toString() { + return this.name; + } +}; + +// By default, both our properties are listed: +for(let key in user) alert(key); // name, toString + +Object.defineProperty(user, "toString", { +*!* + enumerable: false +*/!* +}); + +*!* +// Now toString disappears: +*/!* +for(let key in user) alert(key); // name +``` + +Non-enumerable properties are also excluded from `Object.keys`: + +```js +alert(Object.keys(user)); // name +``` + + + +## Non-configurable + +The non-configurable flag (`configurable:false`) is usually set for built-in objects and properties. + +A non-configurable property can not be deleted or altered with `defineProperty`. + +For instance, `Math.PI` is both read-only, non-enumerable and non-configurable: + +```js run +let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI'); + +alert( JSON.stringify(descriptor, null, 2 ) ); +/* +{ + "value": 3.141592653589793, + "writable": false, + "enumerable": false, + "configurable": false +} +*/ +``` +So, a programmer is unable to change the value of `Math.PI` or overwrite it. + +```js run +Math.PI = 3; // Error + +// delete Math.PI won't work either +``` + +Making a property non-configurable is a one-way road. We cannot change it back, because `defineProperty` doesn't work on non-configurable properties. + +Here we are making `user.name` a "forever sealed" constant: + +```js run +let user = { }; + +Object.defineProperty(user, "name", { + value: "John", + writable: false, + configurable: false +}); + +*!* +// won't be able to change user.name or its flags +// all this won't work: +// user.name = "Pete" +// delete user.name +// defineProperty(user, "name", ...) +Object.defineProperty(user, "name", {writable: true}); // Error +*/!* +``` + +```smart header="Errors appear only in use strict" +In the non-strict mode, no errors occur when writing to read-only properties and such. But the operation still won't succeed. Flag-violating actions are just silently ignored in non-strict. +``` + +## Object.defineProperties + +There's a method [Object.defineProperties(obj, descriptors)](mdn:js/Object/defineProperties) that allows to define many properties at once. + +The syntax is: + +```js +Object.defineProperties(obj, { + prop1: descriptor1, + prop2: descriptor2 + // ... +}); +``` + +For instance: + +```js +Object.defineProperties(user, { + name: { value: "John", writable: false }, + surname: { value: "Smith", writable: false }, + // ... +}); +``` + +So, we can set many properties at once. + +## Object.getOwnPropertyDescriptors + +To get many descriptors at once, we can use the method [Object.getOwnPropertyDescriptors(obj)](mdn:js/Object/getOwnPropertyDescriptors). + +Together with `Object.defineProperties` it can be used as a "flags-aware" way of cloning an object: + +```js +let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj)); +``` + +Normally when we clone an object, we use an assignment to copy properties, like this: + +```js +for(let key in user) { + clone[key] = user[key] +} +``` + +...But that does not copy flags. So if we want a "better" clone then `Object.defineProperties` is preferred. + +## Sealing an object globally + +Property descriptors work at the level of individual properties. + +There are also methods that limit access to the *whole* object: + +[Object.preventExtensions(obj)](mdn:js/Object/preventExtensions) +: Forbids to add properties to the object. + +[Object.seal(obj)](mdn:js/Object/seal) +: Forbids to add/remove properties, sets for all existing properties `configurable: false`. + +[Object.freeze(obj)](mdn:js/Object/freeze) +: Forbids to add/remove/change properties, sets for all existing properties `configurable: false, writable: false`. + +And also there are tests for them: + +[Object.isExtensible(obj)](mdn:js/Object/isExtensible) +: Returns `false` if adding properties is forbidden, otherwise `true`. + +[Object.isSealed(obj)](mdn:js/Object/isSealed) +: Returns `true` if adding/removing properties is forbidden, and all existing properties have `configurable: false`. + +[Object.isFrozen(obj)](mdn:js/Object/isFrozen) +: Returns `true` if adding/removing/changing properties is forbidden, and all current properties are `configurable: false, writable: false`. + +These methods are rarely used in practice. diff --git a/1-js/07-object-oriented-programming/02-property-accessors/article.md b/1-js/07-object-oriented-programming/02-property-accessors/article.md new file mode 100644 index 00000000..616348e7 --- /dev/null +++ b/1-js/07-object-oriented-programming/02-property-accessors/article.md @@ -0,0 +1,239 @@ + +# Property getters and setters + +There are two kinds of properties. + +The first kind is *data properties*. We already know how to work with them. Actually, all properties that we've been using till now were data properties. + +The second type of properties is something new. It's *accessor properties*. They are essentially functions that work on getting and setting a value, but look like regular properties to an external code. + +[cut] + +## Getters and setters + +Accessor properties are represented by "getter" and "setter" methods. In an object literal they are denoted by `get` and `set`: + +```js +let obj = { + *!*get propName()*/!* { + // getter, the code executed on getting obj.propName + }, + + *!*set propName(value)*/!* { + // setter, the code executed on setting obj.propName = value + } +}; +``` + +The getter works when `obj.propName` is read, the setter -- when it is assigned. + +For instance, we have a `user` object with `name` and `surname`: + +```js run +let user = { + name: "John", + surname: "Smith" +}; +``` + +Now we want to add a "fullName" property, that should be "John Smith". Of course, we don't want to copy-paste existing information, so we can implement it as an accessor: + +```js run +let user = { + name: "John", + surname: "Smith", + +*!* + get fullName() { + return `${this.name} ${this.surname}`; + } +*/!* +}; + +*!* +alert(user.fullName); // John Smith +*/!* +``` + +From outside, an accessor property looks like a regular one. That's the idea of accessor properties. We don't *call* `user.fullName` as a function, we *read* it normally: the getter runs behind the scenes. + +As of now, `fullName` has only a getter. If we attempt to assign `user.fullName=`, there will be an error. + +Let's fix it by adding a setter for `user.fullName`: + +```js run +let user = { + name: "John", + surname: "Smith", + + get fullName() { + return `${this.name} ${this.surname}`; + }, + +*!* + set fullName(value) { + [this.name, this.surname] = value.split(" "); + } +*/!* +}; + +// set fullName is executed with the given value. +user.fullName = "Alice Cooper"; + +alert(user.name); // Alice +alert(user.surname); // Cooper +``` + +Now we have a "virtual" property. It is readable and writable, but in fact does not exist. + +```smart header="Accessor properties are only accessible with get/set" +A property can either be a "data property" or an "accessor property", but not both. + +Once a property is defined with `get prop()` or `set prop()`, it's an accessor property. So there must be a getter to read it, and must be a setter if we want to assign it. + +Sometimes it's normal that there's only a setter or only a getter. But the property won't be readable or writable in that case. +``` + + +## Accessor descriptors + +Descriptors for accessor properties are different -- as compared with data properties. + +For accessor properties, there is no `value` and `writable`, but instead there are `get` and `set` functions. + +So an accessor descriptor may have: + +- **`get`** -- a function without arguments, that works when a property is read, +- **`set`** -- a function with one argument, that is called when the property is set, +- **`enumerable`** -- same as for data properties, +- **`configurable`** -- same as for data properties. + +For instance, to create an accessor `fullName` with `defineProperty`, we can pass a descriptor with `get` and `set`: + +```js run +let user = { + name: "John", + surname: "Smith" +}; + +*!* +Object.defineProperty(user, 'fullName', { + get() { + return `${this.name} ${this.surname}`; + }, + + set(value) { + [this.name, this.surname] = value.split(" "); + } +*/!* +}); + +alert(user.fullName); // John Smith + +for(let key in user) alert(key); +``` + +Please note once again that a property can be either an accessor or a data property, not both. + +If we try to supply both `get` and `value` in the same descriptor, there will be an error: + +```js run +*!* +// Error: Invalid property descriptor. +*/!* +Object.defineProperty({}, 'prop', { + get() { + return 1 + }, + + value: 2 +}); +``` + +## Smarter getters/setters + +Getters/setters can be used as wrappers over "real" property values to gain more control over them. + +For instance, if we want to forbid too short names for `user`, we can store `name` in a special property `_name`. And filter assignments in the setter: + +```js run +let user = { + get name() { + return this._name; + }, + + set name(value) { + if (value.length < 4) { + alert("Name is too short, need at least 4 characters"); + return; + } + this._name = value; + } +}; + +user.name = "Pete"; +alert(user.name); // Pete + +user.name = ""; // Name is too short... +``` + +Technically, the external code may still access the name directly by using `user._name`. But there is a widely known agreement that properties starting with an underscore `"_"` are internal and should not be touched from outside the object. + + +## Using for compatibility + +One of the great ideas behind getters and setters -- they allow to take control over a "normal" data property and tweak it at any moment. + +For instance, we started implementing user objects using data properties `name` and `age`: + +```js +function User(name, age) { + this.name = name; + this.age = age; +} + +let john = new User("John", 25); + +alert( john.age ); // 25 +``` + +...But sooner or later, things may change. Instead of `age` we may decide to store `birthday`, because it's more precise and convenient: + +```js +function User(name, birthday) { + this.name = name; + this.birthday = birthday; +} + +let john = new User("John", new Date(1992, 6, 1)); +``` + +Now what to do with the old code that still uses `age` property? + +We can try to find all such places and fix them, but that takes time and can be hard to do if that code is written by other people. And besides, `age` is a nice thing to have in `user`, right? In some places it's just what we want. + +Adding a getter for `age` mitigates the problem: + +```js run no-beautify +function User(name, birthday) { + this.name = name; + this.birthday = birthday; + +*!* + // age is calculated from the current date and birthday + Object.defineProperty(this, "age", { + get() { + let todayYear = new Date().getFullYear(); + return todayYear - this.birthday.getFullYear(); + } + }); +*/!* +} + +let john = new User("John", new Date(1992, 6, 1)); + +alert( john.birthday ); // birthday is available +alert( john.age ); // ...as well as the age +``` + +Now the old code works too and we've got a nice additional property. diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/1-property-after-delete/solution.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/1-property-after-delete/solution.md new file mode 100644 index 00000000..6d25a462 --- /dev/null +++ b/1-js/07-object-oriented-programming/03-prototype-inheritance/1-property-after-delete/solution.md @@ -0,0 +1,4 @@ + +1. `true`, taken from `rabbit`. +2. `null`, taken from `animal`. +3. `undefined`, there's no such property any more. diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/1-property-after-delete/task.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/1-property-after-delete/task.md new file mode 100644 index 00000000..f38fb6f9 --- /dev/null +++ b/1-js/07-object-oriented-programming/03-prototype-inheritance/1-property-after-delete/task.md @@ -0,0 +1,31 @@ +importance: 5 + +--- + +# Working with prototype + +Here's the code that creates a pair of objects, then modifies them. + +Which values are shown in the process? + +```js +let animal = { + jumps: null +}; +let rabbit = { + __proto__: animal, + jumps: true +}; + +alert( rabbit.jumps ); // ? (1) + +delete rabbit.jumps; + +alert( rabbit.jumps ); // ? (2) + +delete animal.jumps; + +alert( rabbit.jumps ); // ? (3) +``` + +There should be 3 answers. diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/2-search-algorithm/solution.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/2-search-algorithm/solution.md new file mode 100644 index 00000000..a16796f9 --- /dev/null +++ b/1-js/07-object-oriented-programming/03-prototype-inheritance/2-search-algorithm/solution.md @@ -0,0 +1,32 @@ + +1. Let's add `__proto__`: + + ```js run + let head = { + glasses: 1 + }; + + let table = { + pen: 3, + __proto__: head + }; + + let bed = { + sheet: 1, + pillow: 2, + __proto__: table + }; + + let pockets = { + money: 2000, + __proto__: bed + }; + + alert( pockets.pen ); // 3 + alert( bed.glasses ); // 1 + alert( table.money ); // undefined + ``` + +2. In modern engines, performance-wise, there's no difference whether we take a property from an object or its prototype. They remember where the property was found and reuse it in the next request. + + For instance, for `pockets.glasses` they remember where they found `glasses` (in `head`), and next time will search right there. They are also smart enough to update internal caches if something changes, so that optimization is safe. diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/2-search-algorithm/task.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/2-search-algorithm/task.md new file mode 100644 index 00000000..70b8b288 --- /dev/null +++ b/1-js/07-object-oriented-programming/03-prototype-inheritance/2-search-algorithm/task.md @@ -0,0 +1,31 @@ +importance: 5 + +--- + +# Searching algorithm + +The task has two parts. + +We have an object: + +```js +let head = { + glasses: 1 +}; + +let table = { + pen: 3 +}; + +let bed = { + sheet: 1, + pillow: 2 +}; + +let pockets = { + money: 2000 +}; +``` + +1. Use `__proto__` to assign prototypes in a way that any property lookup will follow the path: `pockets` -> `bed` -> `table` -> `head`. For instance, `pockets.pen` should be `3` (found in `table`), and `bed.glasses` should be `1` (found in `head`). +2. Answer the question: is it faster to get `glasses` as `pocket.glasses` or `head.glasses`? Benchmark if needed. diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/3-proto-and-this/solution.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/3-proto-and-this/solution.md new file mode 100644 index 00000000..c7d147b9 --- /dev/null +++ b/1-js/07-object-oriented-programming/03-prototype-inheritance/3-proto-and-this/solution.md @@ -0,0 +1,6 @@ +**The answer: `rabbit`.** + +That's because `this` is an object before the dot, so `rabbit.eat()` modifies `rabbit`. + +Property lookup and execution are two different things. +The method `rabbit.eat` is first found in the prototype, then executed with `this=rabbit` diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/3-proto-and-this/task.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/3-proto-and-this/task.md new file mode 100644 index 00000000..b37499ba --- /dev/null +++ b/1-js/07-object-oriented-programming/03-prototype-inheritance/3-proto-and-this/task.md @@ -0,0 +1,23 @@ +importance: 5 + +--- + +# Where it writes? + +We have `rabbit` inheriting from `animal`. + +If we call `rabbit.eat()`, which object receives the `full` property: `animal` or `rabbit`? + +```js +let animal = { + eat() { + this.full = true; + } +}; + +let rabbit = { + __proto__: animal +}; + +rabbit.eat(); +``` diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/4-hamster-proto/solution.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/4-hamster-proto/solution.md new file mode 100644 index 00000000..8ffdb76c --- /dev/null +++ b/1-js/07-object-oriented-programming/03-prototype-inheritance/4-hamster-proto/solution.md @@ -0,0 +1,80 @@ +Let's look carefully at what's going on in the call `speedy.eat("apple")`. + +1. The method `speedy.eat` is found in the prototype (`=hamster`), then executed with `this=speedy` (the object before the dot). + +2. Then `this.stomach.push()` needs to find `stomach` property and call `push` on it. It looks for `stomach` in `this` (`=speedy`), but nothing found. + +3. Then it follows the prototype chain and finds `stomach` in `hamster`. + +4. Then it calls `push` on it, adding the food into *the stomach of the prototype*. + +So all hamsters share a single stomach! + +Every time the `stomach` is taken from the prototype, then `stomach.push` modifies it "at place". + +Please note that such thing doesn't happen in case of a simple assignment `this.stomach=`: + +```js run +let hamster = { + stomach: [], + + eat(food) { +*!* + // assign to this.stomach instead of this.stomach.push + this.stomach = [food]; +*/!* + } +}; + +let speedy = { + __proto__: hamster +}; + +let lazy = { + __proto__: hamster +}; + +// Speedy one found the food +speedy.eat("apple"); +alert( speedy.stomach ); // apple + +// Lazy one's stomach is empty +alert( lazy.stomach ); // +``` + +Now all works fine, because `this.stomach=` does not perform a lookup of `stomach`. The value is written directly into `this` object. + +Also we can totally evade the problem by making sure that each hamster has his own stomach: + +```js run +let hamster = { + stomach: [], + + eat(food) { + this.stomach.push(food); + } +}; + +let speedy = { + __proto__: hamster, +*!* + stomach: [] +*/!* +}; + +let lazy = { + __proto__: hamster, +*!* + stomach: [] +*/!* +}; + +// Speedy one found the food +speedy.eat("apple"); +alert( speedy.stomach ); // apple + +// Lazy one's stomach is empty +alert( lazy.stomach ); // +``` + +As a common solution, all properties that describe the state of a particular object, like `stomach` above, are usually written into that object. That prevents such problems. diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/4-hamster-proto/task.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/4-hamster-proto/task.md new file mode 100644 index 00000000..6f9fb279 --- /dev/null +++ b/1-js/07-object-oriented-programming/03-prototype-inheritance/4-hamster-proto/task.md @@ -0,0 +1,35 @@ +importance: 5 + +--- + +# Why two hamsters are full? + +We have two hamsters: `speedy` and `lazy` inheriting from the general `hamster` object. + +When we feed one of them, the other one is also full. Why? How to fix it? + +```js run +let hamster = { + stomach: [], + + eat(food) { + this.stomach.push(food); + } +}; + +let speedy = { + __proto__: hamster +}; + +let lazy = { + __proto__: hamster +}; + +// This one found the food +speedy.eat("apple"); +alert( speedy.stomach ); // apple + +// This one also has it, why? fix please. +alert( lazy.stomach ); // apple +``` + diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/article.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/article.md new file mode 100644 index 00000000..40dff06b --- /dev/null +++ b/1-js/07-object-oriented-programming/03-prototype-inheritance/article.md @@ -0,0 +1,252 @@ +# Prototypal inheritance + +In programming, we often want to take something and extend it. + +For instance, we have a `user` object with its properties and methods, and want to make `admin` and `guest` as slightly modified variants of it. We'd like to reuse what we have in `user`, not copy/reimplement its methods, just build a new object on top of it. + +*Prototypal inheritance* is a language feature that helps in that. + +[cut] + +## [[Prototype]] + +In JavaScript, objects have a special hidden property `[[Prototype]]` (as named in the specification), that is either `null` or references another object. That object is called "a prototype": + +![prototype](object-prototype-empty.png) + +That `[[Prototype]]` has a "magical" meaning. When we want to read a property from `object`, and it's missing, JavaScript automatically takes it from the prototype. In programming, such thing is called "prototypal inheritance". Many cool language features and programming techniques are based on it. + +The property `[[Prototype]]` is internal and hidden, but there are many ways to set it. + +One of them is to use `__proto__`, like this: + +```js run +let animal = { + eats: true +}; +let rabbit = { + jumps: true +}; + +*!* +rabbit.__proto__ = animal; +*/!* +``` + +Please note that `__proto__` is *not the same* as `[[Prototype]]`. That's a getter/setter for it. We'll talk about other ways of setting it later, but for now `__proto__` will do just fine. + +If we look for a property in `rabbit`, and it's missing, JavaScript automatically takes it from `animal`. + +For instance: + +```js run +let animal = { + eats: true +}; +let rabbit = { + jumps: true +}; + +*!* +rabbit.__proto__ = animal; // (*) +*/!* + +// we can find both properties in rabbit now: +*!* +alert( rabbit.eats ); // true (**) +*/!* +alert( rabbit.jumps ); // true +``` + +Here the line `(*)` sets `animal` to be a prototype of `rabbit`. + +Then, when `alert` tries to read property `rabbit.eats` `(**)`, it's not in `rabbit`, so JavaScript follows the `[[Prototype]]` reference and finds it in `animal` (look from the bottom up): + +![](proto-animal-rabbit.png) + +Here we can say that "`animal` is the prototype of `rabbit`" or "`rabbit` prototypally inherits from `animal`". + +So if `animal` has a lot of useful properties and methods, then they become automatically available in `rabbit`. Such properties are called "inherited". + +If we have a method in `animal`, it can be called on `rabbit`: + +```js run +let animal = { + eats: true, +*!* + walk() { + alert("Animal walk"); + } +*/!* +}; + +let rabbit = { + jumps: true, + __proto__: animal +}; + +// walk is taken from the prototype +*!* +rabbit.walk(); // Animal walk +*/!* +``` + +The method is automatically taken from the prototype, like this: + +![](proto-animal-rabbit-walk.png) + +The prototype chain can be longer: + + +```js run +let animal = { + eats: true, + walk() { + alert("Animal walk"); + } +}; + +let rabbit = { + jumps: true, + __proto__: animal +}; + +let longEar = { + earLength: 10, + __proto__: rabbit +} + +// walk is taken from the prototype chain +longEar.walk(); // Animal walk +alert(longEar.jumps); // true (from rabbit) +``` + +![](proto-animal-rabbit-chain.png) + +There are actually only two limitations: + +1. The references can't go in circles. JavaScript will throw an error if we try to assign `__proto__` in a circle. +2. The value of `__proto__` can be either an object or `null`. All other values (like primitives) are ignored. + +Also it may be obvious, but still: there can be only one `[[Prototype]]`. An object may not inherit from two others. + +## Read/write rules + +The prototype is only used for reading properties. + +For data properties (not getters/setters) write/delete operations work directly with the object. + +In the example below, we assign its own `walk` method to `rabbit`: + +```js run +let animal = { + eats: true, + walk() { + /* this method won't be used by rabbit */ + } +}; + +let rabbit = { + __proto__: animal +} + +*!* +rabbit.walk = function() { + alert("Rabbit! Bounce-bounce!"); +}; +*/!* + +rabbit.walk(); // Rabbit! Bounce-bounce! +``` + +From now on, `rabbit.walk()` call finds the method immediately in the object and executes it, without using the prototype: + +![](proto-animal-rabbit-walk-2.png) + +For getters/setters -- if we read/write a property, they are looked up in the prototype and invoked. + +For instance, check out `admin.fullName` property in the code below: + +```js run +let user = { + name: "John", + surname: "Smith", + + set fullName(value) { + [this.name, this.surname] = value.split(" "); + }, + + get fullName() { + return `${this.name} ${this.surname}`; + } +}; + +let admin = { + __proto__: user, + isAdmin: true +}; + +alert(admin.fullName); // John Smith (*) + +// setter triggers! +admin.fullName = "Alice Cooper"; // (**) +``` + +Here in the line `(*)` the property `admin.fullName` has a getter in the prototype `user`, so it is called. And in the line `(**)` the property has a setter in the prototype, so it is called. + +## The value of "this" + +An interesting question may arise in the example above: what's the value of `this` inside `set fullName(value)`? Where the properties `this.name` and `this.surname` are written: `user` or `admin`? + +The answer is simple: `this` is not affected by prototypes at all. + +**No matter where the method is found: in an object or its prototype. In a method call, `this` is always the object before the dot.** + +So, the setter actually uses `admin` as `this`, not `user`. + +That is actually a super-important thing, because we may have a big object with many methods and inherit from it. Then we can run its methods on inherited objects and they will modify the state of these objects, not the big one. + +For instance, here `animal` represents a "method storage", and `rabbit` makes use of it. + +The call `rabbit.sleep()` sets `this.isSleeping` on the `rabbit` object: + +```js run +// animal has methods +let animal = { + walk() { + if (!this.isSleeping) { + alert(`I walk`); + } + }, + sleep() { + this.isSleeping = true; + } +}; + +let rabbit = { + name: "White Rabbit", + __proto__: animal +}; + +// modifies rabbit.isSleeping +rabbit.sleep(); + +alert(rabbit.isSleeping); // true +alert(animal.isSleeping); // undefined (no such property in the prototype) +``` + +The resulting picture: + +![](proto-animal-rabbit-walk-3.png) + +If we had other objects like `bird`, `snake` etc inheriting from `animal`, they would also gain access to methods of `animal`. But `this` in each method would be the corresponding object, evaluated at the call-time (before dot), not `animal`. So when we write data into `this`, it is stored into these objects. + +As a result, methods are shared, but the object state is not. + +## Summary + +- In JavaScript, all objects have a hidden `[[Prototype]]` property that's either another object or `null`. +- We can use `obj.__proto__` to access it (there are other ways too, to be covered soon). +- The object referenced by `[[Prototype]]` is called a "prototype". +- If we want to read a property of `obj` or call a method, and it doesn't exist, then JavaScript tries to find it in the prototype. Write/delete operations work directly on the object, they don't use the prototype (unless the property is actually a setter). +- If we call `obj.method()`, and the `method` is taken from the prototype, `this` still references `obj`. So methods always work with the current object even if they are inherited. diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/object-prototype-empty.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/object-prototype-empty.png new file mode 100644 index 00000000..1d228b5c Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/object-prototype-empty.png differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/object-prototype-empty@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/object-prototype-empty@2x.png new file mode 100644 index 00000000..a4c195ad Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/object-prototype-empty@2x.png differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-chain.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-chain.png new file mode 100644 index 00000000..a302343e Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-chain.png differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-chain@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-chain@2x.png new file mode 100644 index 00000000..4d331199 Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-chain@2x.png differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-2.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-2.png new file mode 100644 index 00000000..19c1881a Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-2.png differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-2@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-2@2x.png new file mode 100644 index 00000000..cd38624a Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-2@2x.png differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-3.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-3.png new file mode 100644 index 00000000..0ea869d3 Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-3.png differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-3@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-3@2x.png new file mode 100644 index 00000000..194db77e Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-3@2x.png differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk.png new file mode 100644 index 00000000..5bd21114 Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk.png differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk@2x.png new file mode 100644 index 00000000..1e4ef6ef Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk@2x.png differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit.png new file mode 100644 index 00000000..c18e0205 Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit.png differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit@2x.png new file mode 100644 index 00000000..4d9e8ec0 Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit@2x.png differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-user-admin.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-user-admin.png new file mode 100644 index 00000000..aed69c59 Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-user-admin.png differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-user-admin@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-user-admin@2x.png new file mode 100644 index 00000000..2b11b256 Binary files /dev/null and b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-user-admin@2x.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/1-changing-prototype/solution.md b/1-js/07-object-oriented-programming/04-function-prototype/1-changing-prototype/solution.md new file mode 100644 index 00000000..771e3061 --- /dev/null +++ b/1-js/07-object-oriented-programming/04-function-prototype/1-changing-prototype/solution.md @@ -0,0 +1,20 @@ + +Answers: + +1. `true`. + + The assignment to `Rabbit.prototype` sets up `[[Prototype]]` for new objects, but it does not affect the existing ones. + +2. `false`. + + Objects are assigned by reference. The object from `Rabbit.prototype` is not duplicated, it's still a single object is referenced both by `Rabbit.prototype` and by the `[[Prototype]]` of `rabbit`. + + So when we change its content through one reference, it is visible through the other one. + +3. `true`. + + All `delete` operations are applied directly to the object. Here `delete rabbit.eats` tries to remove `eats` property from `rabbit`, but it doesn't have it. So the operation won't have any effect. + +4. `undefined`. + + The property `eats` is deleted from the prototype, it doesn't exist any more. diff --git a/1-js/07-object-oriented-programming/04-function-prototype/1-changing-prototype/task.md b/1-js/07-object-oriented-programming/04-function-prototype/1-changing-prototype/task.md new file mode 100644 index 00000000..4b8522d3 --- /dev/null +++ b/1-js/07-object-oriented-programming/04-function-prototype/1-changing-prototype/task.md @@ -0,0 +1,89 @@ +importance: 5 + +--- + +# Changing "prototype" + +In the code below we create `new Rabbit`, and then try to modify its prototype. + +In the start, we have this code: + +```js run +function Rabbit() {} +Rabbit.prototype = { + eats: true +}; + +let rabbit = new Rabbit(); + +alert( rabbit.eats ); // true +``` + + +1. We added one more string (emphasized), what `alert` shows now? + + ```js + function Rabbit() {} + Rabbit.prototype = { + eats: true + }; + + let rabbit = new Rabbit(); + + *!* + Rabbit.prototype = {}; + */!* + + alert( rabbit.eats ); // ? + ``` + +2. ...And if the code is like this (replaced one line)? + + ```js + function Rabbit() {} + Rabbit.prototype = { + eats: true + }; + + let rabbit = new Rabbit(); + + *!* + Rabbit.prototype.eats = false; + */!* + + alert( rabbit.eats ); // ? + ``` + +3. Like this (replaced one line)? + + ```js + function Rabbit() {} + Rabbit.prototype = { + eats: true + }; + + let rabbit = new Rabbit(); + + *!* + delete rabbit.eats; + */!* + + alert( rabbit.eats ); // ? + ``` + +4. The last variant: + + ```js + function Rabbit() {} + Rabbit.prototype = { + eats: true + }; + + let rabbit = new Rabbit(); + + *!* + delete Rabbit.prototype.eats; + */!* + + alert( rabbit.eats ); // ? + ``` diff --git a/1-js/07-object-oriented-programming/04-function-prototype/4-new-object-same-constructor/solution.md b/1-js/07-object-oriented-programming/04-function-prototype/4-new-object-same-constructor/solution.md new file mode 100644 index 00000000..43190e16 --- /dev/null +++ b/1-js/07-object-oriented-programming/04-function-prototype/4-new-object-same-constructor/solution.md @@ -0,0 +1,44 @@ +We can use such approach if we are sure that `"constructor"` property has the correct value. + +For instance, if we don't touch the default `"prototype"`, then this code works for sure: + +```js run +function User(name) { + this.name = name; +} + +let user = new User('John'); +let user2 = new user.constructor('Pete'); + +alert( user2.name ); // Pete (worked!) +``` + +It worked, because `User.prototype.constructor == User`. + +..But if someone, so to say, overwrites `User.prototype` and forgets to recreate `"constructor"`, then it would fail. + +For instance: + +```js run +function User(name) { + this.name = name; +} +*!* +User.prototype = {}; // (*) +*/!* + +let user = new User('John'); +let user2 = new user.constructor('Pete'); + +alert( user2.name ); // undefined +``` + +Why `user2.name` is `undefined`? + +Here's how `new user.constructor('Pete')` works: + +1. First, it looks for `constructor` in `user`. Nothing. +2. Then it follows the prototype chain. The prototype of `user` is `User.prototype`, and it also has nothing. +3. The value of `User.prototype` is a plain object `{}`, its prototype is `Object.prototype`. And there is `Object.prototype.constructor == Object`. So it is used. + +At the end, we have `let user2 = new Object('Pete')`. The built-in `Object` constructor ignores arguments, it always creates an empty object -- that's what we have in `user2` after all. diff --git a/1-js/07-object-oriented-programming/04-function-prototype/4-new-object-same-constructor/task.md b/1-js/07-object-oriented-programming/04-function-prototype/4-new-object-same-constructor/task.md new file mode 100644 index 00000000..934f3470 --- /dev/null +++ b/1-js/07-object-oriented-programming/04-function-prototype/4-new-object-same-constructor/task.md @@ -0,0 +1,15 @@ +importance: 5 + +--- + +# Create an object with the same constructor + +Imagine, we have an arbitrary object `obj`, created by a constructor function -- we don't know which one, but we'd like to create a new object using it. + +Can we do it like that? + +```js +let obj2 = new obj.constructor(); +``` + +Give an example of a constructor function for `obj` which lets such code work right. And an example that makes it work wrong. diff --git a/1-js/07-object-oriented-programming/04-function-prototype/article.md b/1-js/07-object-oriented-programming/04-function-prototype/article.md new file mode 100644 index 00000000..27c67e32 --- /dev/null +++ b/1-js/07-object-oriented-programming/04-function-prototype/article.md @@ -0,0 +1,176 @@ +# F.prototype + +In modern JavaScript we can set a prototype using `__proto__`. But it wasn't like that all the time. + +[cut] + +JavaScript has had prototypal inheritance from the beginning. It was one of the core features of the language. + +But in the old times, there was another (and the only) way to set it: to use a `"prototype"` property of the constructor function. And there are still many scripts that use it. + +## The "prototype" property + +As we know already, `new F()` creates a new object. But what we didn't use yet `F.prototype` property. + +That property is used by the JavaScript itself to set `[[Prototype]]` for new objects. + +**When a new object is created with `new F()`, the object's `[[Prototype]]` is set to `F.prototype`.** + +Please note that `F.prototype` here means a regular property named `"prototype"` on `F`. It sounds something similar to the term "prototype", but here we really mean a regular property with this name. + +Here's the example: + +```js run +let animal = { + eats: true +}; + +function Rabbit(name) { + this.name = name; +} + +*!* +Rabbit.prototype = animal; +*/!* + +let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal + +alert( rabbit.eats ); // true +``` + +Setting `Rabbit.prototype = animal` literally states the following: "When a `new Rabbit` is created, assign its `[[Prototype]]` to `animal`". + +That's the resulting picture: + +![](proto-constructor-animal-rabbit.png) + +On the picture, `"prototype"` is a horizontal arrow, it's a regular property, and `[[Prototype]]` is vertical, meaning the inheritance of `rabbit` from `animal`. + + +## Default F.prototype, constructor property + +Every function has the `"prototype"` property even if we don't supply it. + +The default `"prototype"` is an object with the only property `constructor` that points back to the function itself. + +Like this: + +```js +function Rabbit() {} + +/* default prototype +Rabbit.prototype = { constructor: Rabbit }; +*/ +``` + +![](function-prototype-constructor.png) + +We can check it: + +```js run +function Rabbit() {} +// by default: +// Rabbit.prototype = { constructor: Rabbit } + +alert( Rabbit.prototype.constructor == Rabbit ); // true +``` + +Naturally, it we do nothing, the `constructor` property is available to all rabbits through `[[Prototype]]`: + +```js run +function Rabbit() {} +// by default: +// Rabbit.prototype = { constructor: Rabbit } + +let rabbit = new Rabbit(); // inherits from {constructor: Rabbit} + +alert(rabbit.constructor == Rabbit); // true (from prototype) +``` + +![](rabbit-prototype-constructor.png) + +We can use `constructor` property to create a new object using the same constructor as the existing one. + +Like here: + +```js run +function Rabbit(name) { + this.name = name; + alert(name); +} + +let rabbit = new Rabbit("White Rabbit"); + +*!* +let rabbit2 = new rabbit.constructor("Black Rabbit"); +*/!* +``` + +That's handy when we have an object, don't know which constructor was used for it (e.g. it comes from a 3rd party library), and we need to create another one of the same kind. + +But probably the most important thing about `"constructor"` is that... + +**...JavaScript itself does not ensure the right `"constructor"` value.** + +Yes, it exists in the default `"prototype"` for functions, but that's all. What happens with it later -- is totally on us. + +In particular, if we replace the default prototype as a whole, then there will be no `"constructor"` in it. + +For instance: + +```js run +function Rabbit() {} +Rabbit.prototype = { + jumps: true +}; + +let rabbit = new Rabbit(); +*!* +alert(rabbit.constructor === Rabbit); // false +*/!* +``` + +So, to keep the right `"constructor"` we can choose to add/remove properties to the default `"prototype"` instead of overwriting it as a whole: + +```js +function Rabbit() {} + +// Not overwrite Rabbit.prototype totally +// just add to it +Rabbit.prototype.jumps = true +// the default Rabbit.prototype.constructor is preserved +``` + +Or, alternatively, recreate the `constructor` property it manually: + +```js +Rabbit.prototype = { + jumps: true, +*!* + constructor: Rabbit +*/!* +}; + +// now constructor is also correct, because we added it +``` + + +## Summary + +In this chapter we briefly described the way of setting a `[[Prototype]]` for objects created via a constructor function. Later we'll see more advanced programming patterns that rely on it. + +Everything is quite simple, just few notes to make things clear: + +- The `F.prototype` property is not the same as `[[Prototype]]`. The only thing `F.prototype` does: it sets `[[Prototype]]` of new objects when `new F()` is called. +- The value of `F.prototype` should be either an object or null: other values won't work. +- The `"prototype"` property only has such a special effect when is set to a constructor function, and invoked with `new`. + +On regular objects the `prototype` is nothing special: +```js +let user = { + name: "John", + prototype: "Bla-bla" // no magic at all +}; +``` + +By default all functions have `F.prototype = { constructor: F }`, so we can get the constructor of an object by accessing its `"constructor"` property. diff --git a/1-js/07-object-oriented-programming/04-function-prototype/function-prototype-constructor.png b/1-js/07-object-oriented-programming/04-function-prototype/function-prototype-constructor.png new file mode 100644 index 00000000..0dbc7b8f Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/function-prototype-constructor.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/function-prototype-constructor@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/function-prototype-constructor@2x.png new file mode 100644 index 00000000..e38cb85b Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/function-prototype-constructor@2x.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-array-tostring.png b/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-array-tostring.png new file mode 100644 index 00000000..83258d06 Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-array-tostring.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-array-tostring@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-array-tostring@2x.png new file mode 100644 index 00000000..d77cc8f4 Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-array-tostring@2x.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-classes.png b/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-classes.png new file mode 100644 index 00000000..fa6a2943 Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-classes.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-classes@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-classes@2x.png new file mode 100644 index 00000000..9368d7e5 Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-classes@2x.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/object-prototype-1.png b/1-js/07-object-oriented-programming/04-function-prototype/object-prototype-1.png new file mode 100644 index 00000000..e7c6bdb2 Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/object-prototype-1.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/object-prototype-1@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/object-prototype-1@2x.png new file mode 100644 index 00000000..34c256bb Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/object-prototype-1@2x.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/object-prototype.png b/1-js/07-object-oriented-programming/04-function-prototype/object-prototype.png new file mode 100644 index 00000000..d97d87d2 Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/object-prototype.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/object-prototype@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/object-prototype@2x.png new file mode 100644 index 00000000..76ce4c9a Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/object-prototype@2x.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/proto-constructor-animal-rabbit.png b/1-js/07-object-oriented-programming/04-function-prototype/proto-constructor-animal-rabbit.png new file mode 100644 index 00000000..3eec740f Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/proto-constructor-animal-rabbit.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/proto-constructor-animal-rabbit@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/proto-constructor-animal-rabbit@2x.png new file mode 100644 index 00000000..ed28a388 Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/proto-constructor-animal-rabbit@2x.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-animal-object.png b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-animal-object.png new file mode 100644 index 00000000..3254270f Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-animal-object.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-animal-object@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-animal-object@2x.png new file mode 100644 index 00000000..f794d7b8 Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-animal-object@2x.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor.png b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor.png new file mode 100644 index 00000000..d3ef3448 Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor@2x.png new file mode 100644 index 00000000..45cbb6ba Binary files /dev/null and b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor@2x.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/1-defer-to-prototype/solution.md b/1-js/07-object-oriented-programming/05-native-prototypes/1-defer-to-prototype/solution.md new file mode 100644 index 00000000..ebd2f44e --- /dev/null +++ b/1-js/07-object-oriented-programming/05-native-prototypes/1-defer-to-prototype/solution.md @@ -0,0 +1,13 @@ + + +```js run +Function.prototype.defer = function(ms) { + setTimeout(this, ms); +}; + +function f() { + alert("Hello!"); +} + +f.defer(1000); // shows "Hello!" after 1 sec +``` diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/1-defer-to-prototype/task.md b/1-js/07-object-oriented-programming/05-native-prototypes/1-defer-to-prototype/task.md new file mode 100644 index 00000000..d3b3a51c --- /dev/null +++ b/1-js/07-object-oriented-programming/05-native-prototypes/1-defer-to-prototype/task.md @@ -0,0 +1,17 @@ +importance: 5 + +--- + +# Add method "f.defer(ms)" to functions + +Add to the prototype of all functions the method `defer(ms)`, that runs the function after `ms` milliseconds. + +After you do it, such code should work: + +```js +function f() { + alert("Hello!"); +} + +f.defer(1000); // shows "Hello!" after 1 second +``` diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/2-defer-to-prototype-extended/solution.md b/1-js/07-object-oriented-programming/05-native-prototypes/2-defer-to-prototype-extended/solution.md new file mode 100644 index 00000000..e3651683 --- /dev/null +++ b/1-js/07-object-oriented-programming/05-native-prototypes/2-defer-to-prototype-extended/solution.md @@ -0,0 +1,17 @@ + + +```js run +Function.prototype.defer = function(ms) { + let f = this; + return function(...args) { + setTimeout(() => f.apply(this, args), ms); + } +}; + +// check it +function f(a, b) { + alert( a + b ); +} + +f.defer(1000)(1, 2); // shows 3 after 1 sec +``` diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/2-defer-to-prototype-extended/task.md b/1-js/07-object-oriented-programming/05-native-prototypes/2-defer-to-prototype-extended/task.md new file mode 100644 index 00000000..4d3823bb --- /dev/null +++ b/1-js/07-object-oriented-programming/05-native-prototypes/2-defer-to-prototype-extended/task.md @@ -0,0 +1,19 @@ +importance: 4 + +--- + +# Add the decorating "defer()" to functions + +Add to the prototype of all functions the method `defer(ms)`, that returns a wrapper, delaying the call by `ms` milliseconds. + +Here's an example of how it should work: + +```js +function f(a, b) { + alert( a + b ); +} + +f.defer(1000)(1, 2); // shows 3 after 1 second +``` + +Please note that the arguments should be passed to the original function. diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/article.md b/1-js/07-object-oriented-programming/05-native-prototypes/article.md new file mode 100644 index 00000000..6c29c7f1 --- /dev/null +++ b/1-js/07-object-oriented-programming/05-native-prototypes/article.md @@ -0,0 +1,180 @@ +# Native prototypes + +The `"prototype"` property is widely used by the core of JavaScript itself. All built-in constructor functions use it. + +We'll see how it is for plain objects first, and then for more complex ones. + +## Object.prototype + +Let's say we output an empty object: + +```js run +let obj = {}; +alert( obj ); // "[object Object]" ? +``` + +Where's the code that generates the string `"[object Object]"`? That's a built-in `toString` method, but where is it? The `obj` is empty! + +...But the short notation `obj = {}` is the same as `obj = new Object()`, where `Object` -- is a built-in object constructor function. And that function has `Object.prototype` that references a huge object with `toString` and other functions. + +Like this (all that is built-in): + +![](object-prototype.png) + +When `new Object()` is called (or a literal object `{...}` is created), the `[[Prototype]]` of it is set to `Object.prototype` by the rule that we've discussed in the previous chapter: + +![](object-prototype-1.png) + +Afterwards when `obj.toString()` is called -- the method is taken from `Object.prototype`. + +We can check it like this: + +```js run +let obj = {}; + +alert(obj.__proto__ === Object.prototype); // true +// obj.toString === obj.__proto__toString == Object.prototype.toString +``` + +Please note that there is no additional `[[Prototype]]` in the chain above `Object.prototype`: + +```js run +alert(Object.prototype.__proto__); // null +``` + +## Other built-in prototypes + +Other built-in objects such as `Array`, `Date`, `Function` and others also keep methods in prototypes. + +For instance, when we create an array `[1, 2, 3]`, the default `new Array()` constructor is used internally. So the array data is written into the new object, and `Array.prototype` becomes its prototype and provides methods. That's very memory-efficient. + +By specification, all built-in prototypes have `Object.prototype` on the top. Sometimes people say that "everything inherits from objects". + +Here's the overall picture (for 3 built-ins to fit): + +![](native-prototypes-classes.png) + +Let's check the prototypes manually: + +```js run +let arr = [1, 2, 3]; + +// it inherits from Array.prototype? +alert( arr.__proto__ === Array.prototype ); // true + +// then from Object.prototype? +alert( arr.__proto__.__proto__ === Object.prototype ); // true + +// and null on the top. +alert( arr.__proto__.__proto__.__proto__ ); // null +``` + +Some methods in prototypes may overlap, for instance, `Array.prototype` has its own `toString` that lists comma-delimited elements: + +```js run +let arr = [1, 2, 3] +alert(arr); // 1,2,3 <-- the result of Array.prototype.toString +``` + +As we've seen before, `Object.prototype` has `toString` as well, but `Array.prototype` is closer in the chain, so the array variant is used. + + +![](native-prototypes-array-tostring.png) + + +In-browser tools like Chrome developer console also show inheritance (may need to use `console.dir` for built-in objects): + +![](console_dir_array.png) + +Other built-in objects also work the same way. Even functions. They are objects of a built-in `Function` constructor, and their methods: `call/apply` and others are taken from `Function.prototype`. Functions have their own `toString` too. + +```js run +function f() {} + +alert(f.__proto__ == Function.prototype); // true +alert(f.__proto__.__proto__ == Object.prototype); // true, inherit from objects +``` + +## Primitives + +The most intricate thing happens with strings, numbers and booleans. + +As we remember, they are not objects. But if we try to access their properties, then temporary wrapper objects are created using built-in constructors `String`, `Number`, `Boolean`, they provide the methods and disappear. + +These objects are created invisibly to us and most engines optimize them out, but the specification describes it exactly this way. Methods of these objects also reside in prototypes, available as `String.prototype`, `Number.prototype` and `Boolean.prototype`. + +```warn header="Values `null` and `undefined` have no object wrappers" +Special values `null` and `undefined` stand apart. They have no object wrappers, so methods and properties are not available for them. And there are no corresponding prototypes too. +``` + +## Changing native prototypes [#native-prototype-change] + +Native prototypes can be modified. For instance, if we add a method to `String.prototype`, it becomes available to all strings: + +```js run +String.prototype.show = function() { + alert(this); +}; + +"BOOM!".show(); // BOOM! +``` + +During the process of development we may have ideas which new built-in methods we'd like to have. And there may be a slight temptation to add them to native prototypes. But that is generally a bad idea. + +Prototypes are global, so it's easy to get a conflict. If two libraries add a method `String.prototype.show`, then one of them overwrites the other one. + +In modern programming, there is only one case when modifying native prototypes is approved. That's polyfills. In other words, if there's a method in JavaScript specification that is not yet supported by our JavaScript engine (or any of those that we want to support), then may implement it manually and populate the built-in prototype with it. + +For instance: + +```js run +if (!String.prototype.repeat) { // if there's no such method + // add it to the prototype + + String.prototype.repeat = function(n) { + // repeat the string n times + + // actually, the code should be more complex than that, + // throw errors for negative values of "n" + // the full algorithm is in the specification + return new Array(n + 1).join(this); + }; +} + +alert( "La".repeat(3) ); // LaLaLa +``` + +## Borrowing from prototypes + +In the chapter we talked about method borrowing: + +```js run +function showArgs() { +*!* + // borrow join from array and call in the context of arguments + alert( [].join.call(arguments, " - ") ); +*/!* +} + +showArgs("John", "Pete", "Alice"); // John - Pete - Alice +``` + +Because `join` resides in `Array.prototype`, we can call it from there directly and rewrite it as: + +```js +function showArgs() { +*!* + alert( Array.prototype.join.call(arguments, " - ") ); +*/!* +} +``` + +That's more efficient, because it avoids the creation of an extra array object `[]`. On the other hand, it is longer to write. + +## Summary + +- All built-in objects follow the same pattern: + - The methods are stored in the prototype (`Array.prototype`, `Object.prototype`, `Date.prototype` etc). + - The object itself stores only the data (array items, object properties, the date). +- Primitives also store methods in prototypes of wrapper objects: `Number.prototype`, `String.prototype`, `Boolean.prototype`. There are no wrapper objects only for `undefined` and `null`. +- Built-in prototypes can be modified or populated with new methods. But it's not recommended to change them. Probably the only allowable cause is when we add-in a new standard, but not yet supported by the engine JavaScript method. diff --git a/1-js/9-prototypes/5-class-inheritance/console_dir_array.png b/1-js/07-object-oriented-programming/05-native-prototypes/console_dir_array.png old mode 100755 new mode 100644 similarity index 100% rename from 1-js/9-prototypes/5-class-inheritance/console_dir_array.png rename to 1-js/07-object-oriented-programming/05-native-prototypes/console_dir_array.png diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/function-prototype-constructor.png b/1-js/07-object-oriented-programming/05-native-prototypes/function-prototype-constructor.png new file mode 100644 index 00000000..0dbc7b8f Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/function-prototype-constructor.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/function-prototype-constructor@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/function-prototype-constructor@2x.png new file mode 100644 index 00000000..e38cb85b Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/function-prototype-constructor@2x.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-array-tostring.png b/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-array-tostring.png new file mode 100644 index 00000000..83258d06 Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-array-tostring.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-array-tostring@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-array-tostring@2x.png new file mode 100644 index 00000000..d77cc8f4 Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-array-tostring@2x.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-classes.png b/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-classes.png new file mode 100644 index 00000000..fa6a2943 Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-classes.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-classes@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-classes@2x.png new file mode 100644 index 00000000..9368d7e5 Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-classes@2x.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-1.png b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-1.png new file mode 100644 index 00000000..e7c6bdb2 Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-1.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-1@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-1@2x.png new file mode 100644 index 00000000..34c256bb Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-1@2x.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-null.png b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-null.png new file mode 100644 index 00000000..7c2e3f9c Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-null.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-null@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-null@2x.png new file mode 100644 index 00000000..fec1facb Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-null@2x.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype.png b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype.png new file mode 100644 index 00000000..d97d87d2 Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype@2x.png new file mode 100644 index 00000000..76ce4c9a Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype@2x.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/proto-constructor-animal-rabbit.png b/1-js/07-object-oriented-programming/05-native-prototypes/proto-constructor-animal-rabbit.png new file mode 100644 index 00000000..3eec740f Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/proto-constructor-animal-rabbit.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/proto-constructor-animal-rabbit@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/proto-constructor-animal-rabbit@2x.png new file mode 100644 index 00000000..ed28a388 Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/proto-constructor-animal-rabbit@2x.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor.png b/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor.png new file mode 100644 index 00000000..d3ef3448 Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor@2x.png new file mode 100644 index 00000000..45cbb6ba Binary files /dev/null and b/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor@2x.png differ diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/solution.md b/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/solution.md new file mode 100644 index 00000000..debaecd6 --- /dev/null +++ b/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/solution.md @@ -0,0 +1,29 @@ + +The method can take all enumerable keys using `Object.keys` and output their list. + +To make `toString` non-enumerable, let's define it using a property descriptor. The syntax of `Object.create` allows to provide an object with property descriptors as the second argument. + +```js run +*!* +let dictionary = Object.create(null, { + toString: { // define toString property + value() { // the value is a function + return Object.keys(this).join(); + } + } +}); +*/!* + +dictionary.apple = "Apple"; +dictionary.__proto__ = "test"; + +// apple and __proto__ is in the loop +for(let key in dictionary) { + alert(key); // "apple", then "__proto__" +} + +// comma-separated list of properties by toString +alert(dictionary); // "apple,__proto__" +``` + +When we create a property using a descriptor, its flags are `false` by default. So in the code above, `dictionary.toString` is non-enumerable. diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/task.md b/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/task.md new file mode 100644 index 00000000..0d831f2c --- /dev/null +++ b/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/task.md @@ -0,0 +1,31 @@ +importance: 5 + +--- + +# Add toString to the dictionary + +There's an object `dictionary`, created as `Object.create(null)`, to store any `key/value` pairs. + +Add method `dictionary.toString()` into it, that should return a comma-delimited list of keys. Your `toString` should not show up in `for..in` over the object. + +Here's how it should work: + +```js +let dictionary = Object.create(null); + +*!* +// your code to add dictionary.toString method +*/!* + +// add some data +dictionary.apple = "Apple"; +dictionary.__proto__ = "test"; // __proto__ is a regular property key here + +// only apple and __proto__ are in the loop +for(let key in dictionary) { + alert(key); // "apple", then "__proto__" +} + +// your toString in action +alert(dictionary); // "apple,__proto__" +``` diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/solution.md b/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/solution.md new file mode 100644 index 00000000..90d3118b --- /dev/null +++ b/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/solution.md @@ -0,0 +1,20 @@ + +The first call has `this == rabbit`, the other ones have `this` equal to `Rabbit.prototype`, because it's actually the object before the dot. + +So only the first call shows `Rabbit`, other ones show `undefined`: + +```js run +function Rabbit(name) { + this.name = name; +} +Rabbit.prototype.sayHi = function() { + alert( this.name ); +} + +let rabbit = new Rabbit("Rabbit"); + +rabbit.sayHi(); // Rabbit +Rabbit.prototype.sayHi(); // undefined +Object.getPrototypeOf(rabbit).sayHi(); // undefined +rabbit.__proto__.sayHi(); // undefined +``` diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/task.md b/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/task.md new file mode 100644 index 00000000..92653bd8 --- /dev/null +++ b/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/task.md @@ -0,0 +1,27 @@ +importance: 5 + +--- + +# The difference beteeen calls + +Let's create a new `rabbit` object: + +```js +function Rabbit(name) { + this.name = name; +} +Rabbit.prototype.sayHi = function() { + alert(this.name); +}; + +let rabbit = new Rabbit("Rabbit"); +``` + +These calls do the same thing or not? + +```js +rabbit.sayHi(); +Rabbit.prototype.sayHi(); +Object.getPrototypeOf(rabbit).sayHi(); +rabbit.__proto__.sayHi(); +``` diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/article.md b/1-js/07-object-oriented-programming/06-prototype-methods/article.md new file mode 100644 index 00000000..ec14fd48 --- /dev/null +++ b/1-js/07-object-oriented-programming/06-prototype-methods/article.md @@ -0,0 +1,255 @@ + +# Methods for prototypes + +In this chapter we cover additional methods to work with a prototype. + +There are also other ways to get/set a prototype, besides those that we already know: + +- [Object.create(proto[, descriptors])](mdn:js/Object/create) -- creates an empty object with given `proto` as `[[Prototype]]` and optional property descriptors. +- [Object.getPrototypeOf(obj)](mdn:js/Object.getPrototypeOf) -- returns the `[[Prototype]]` of `obj`. +- [Object.setPrototypeOf(obj, proto)](mdn:js/Object.setPrototypeOf) -- sets the `[[Prototype]]` of `obj` to `proto`. + +[cut] + +For instance: + +```js run +let animal = { + eats: true +}; + +// create a new object with animal as a prototype +*!* +let rabbit = Object.create(animal); +*/!* + +alert(rabbit.eats); // true +*!* +alert(Object.getPrototypeOf(rabbit) === animal); // get the prototype of rabbit +*/!* + +*!* +Object.setPrototypeOf(rabbit, {}); // change the prototype of rabbit to {} +*/!* +``` + +`Object.create` has an optional second argument: property descriptors. We can provide additional properties to the new object there, like this: + +```js run +let animal = { + eats: true +}; + +let rabbit = Object.create(animal, { + jumps: { + value: true + } +}); + +alert(rabbit.jumps); // true +``` + +The descriptors are in the same format as described in the chapter . + +We can use `Object.create` to perform a full object cloning, like this: + +```js +// fully identical shallow clone of obj +let clone = Object.create(obj, Object.getOwnPropertyDescriptors(obj)); +``` + +This call makes a truly exact copy of `obj`, including all properties: enumerable and non-enumerable, data properties and setters/getters -- everything, and with the right `[[Prototype]]`. But not an in-depth copy of course. + +## Brief history + +If we count all the ways to manage `[[Prototype]]`, there's a lot! Many ways to do the same! + +Why so? + +That's for historical reasons. + +- The `"prototype"` property of a constructor function works since very ancient times. +- Later in the year 2012: `Object.create` appeared in the standard. It allowed to create objects with the given prototype, but did not allow to get/set it. So browsers implemented non-standard `__proto__` accessor that allowed to get/set a prototype at any time. +- Later in the year 2015: `Object.setPrototypeOf` and `Object.getPrototypeOf` were added to the standard. The `__proto__` was de-facto implemented everywhere, so it made its way to the Annex B of the standard, that is optional for non-browser environments. + +As of now we have all these ways at our disposal. + +Technically, we can get/set `[[Prototype]]` at any time. But usually we only set it once at the object creation time, and then do not modify: `rabbit` inherits from `animal`, and that is not going to change. And JavaScript engines are highly optimized to that. Changing a prototype "on-the-fly" with `Object.setPrototypeOf` or `obj.__proto__=` is a very slow operation. But it is possible. + +## "Very plain" objects + +As we know, objects can be used as associative arrays to store key/value pairs. + +...But if we try to store *user-provided* keys in it (for instance, a user-entered dictionary), we can see an interesting glitch: all keys work fine except `"__proto__"`. + +Check out the example: + +```js run +let obj = {}; + +let key = prompt("What's the key?", "__proto__"); +obj[key] = "some value"; + +alert(obj[key]); // [object Object], not "some value"! +``` + +Here if the user types in `__proto__`, the assignment is ignored! + +That shouldn't surprise us. The `__proto__` property is special: it must be either an object or `null`, a string can not become a prototype. + +But we did not intend to implement such behavior, right? We want to store key/value pairs, and the key named `"__proto__"` was not properly saved. So that's a bug. Here the consequences are not terrible. But in other cases the prototype may indeed be changed, so the execution may go wrong in totally unexpected ways. + +What's worst -- usually developers do not think about such possibility at all. That makes such bugs hard to notice and even turn them into vulnerabilities, especially when JavaScript is used on server-side. + +Such thing happens only with `__proto__`. All other properties are "assignable" normally. + +How to evade the problem? + +First, we can just switch to using `Map`, then everything's fine. + +But `Object` also can serve us well here, because language creators gave a thought to that problem long ago. + +The `__proto__` is not a property of an object, but an accessor property of `Object.prototype`: + +![](object-prototype-2.png) + +So, if `obj.__proto__` is read or assigned, the corresponding getter/setter is called from its prototype, and it gets/sets `[[Prototype]]`. + +As it was said in the beginning: `__proto__` is a way to access `[[Prototype]]`, it is not `[[Prototype]]` itself. + +Now, if we want to use an object as an associative array, we can do it with a little trick: + +```js run +*!* +let obj = Object.create(null); +*/!* + +let key = prompt("What's the key?", "__proto__"); +obj[key] = "some value"; + +alert(obj[key]); // "some value" +``` + +`Object.create(null)` creates an empty object without a prototype (`[[Prototype]]` is `null`): + +![](object-prototype-null.png) + +So, there is no inherited getter/setter for `__proto__`. Now it is processed as a regular data property, so the example above works right. + +We can call such object "very plain" or "pure dictionary objects", because they are even simpler than regular plain object `{...}`. + +A downside is that such objects lack any built-in object methods, e.g. `toString`: + +```js run +*!* +let obj = Object.create(null); +*/!* + +alert(obj); // Error (no toString) +``` + +...But that's usually fine for associative arrays. + +Please note that most object-related methods are `Object.something(...)`, like `Object.keys(obj)` -- they are not in the prototype, so they will keep working on such objects: + + +```js run +let chineseDictionary = Object.create(null); +chineseDictionary.hello = "ni hao"; +chineseDictionary.bye = "zai jian"; + +alert(Object.keys(chineseDictionary)); // hello,bye +``` + +## Getting all properties + +There are many ways to get keys/values from an object. + +We already know these ones: + +- [Object.keys(obj)](mdn:js/Object/keys) / [Object.values(obj)](mdn:js/Object/values) / [Object.entries(obj)](mdn:js/Object/entries) -- returns an array of enumerable own string property names/values/key-value pairs. These methods only list *enumerable* properties, and those that have *strings as keys*. + +If we want symbolic properties: + +- [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols) -- returns an array of all own symbolic property names. + +If we want non-enumerable properties: + +- [Object.getOwnPropertyNames(obj)](mdn:js/Object/getOwnPropertyNames) -- returns an array of all own string property names. + +If we want *all* properties: + +- [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) -- returns an array of all own property names. + +These methods are a bit different about which properties they return, but all of them operate on the object itself. Properties from the prototype are not listed. + +The `for..in` loop is different: it loops over inherited properties too. + +For instance: + +```js run +let animal = { + eats: true +}; + +let rabbit = { + jumps: true, + __proto__: animal +}; + +*!* +// only own keys +alert(Object.keys(rabbit)); // jumps +*/!* + +*!* +// inherited keys too +for(let prop in rabbit) alert(prop); // jumps, then eats +*/!* +``` + +If we want to distinguish inherited properties, there's a built-in method [obj.hasOwnProperty(key)](mdn:js/Object/hasOwnProperty): it returns `true` if `obj` has its own (not inherited) property named `key`. + +So we can filter out inherited properties (or do something else with them): + +```js run +let animal = { + eats: true +}; + +let rabbit = { + jumps: true, + __proto__: animal +}; + +for(let prop in rabbit) { + let isOwn = rabbit.hasOwnProperty(prop); + alert(`${prop}: ${isOwn}`); // jumps:true, then eats:false +} +``` +Here we have the following inheritance chain: `rabbit`, then `animal`, then `Object.prototype` (because `animal` is a literal object `{...}`, so it's by default), and then `null` above it: + +![](rabbit-animal-object.png) + +Note, there's one funny thing. Where is the method `rabbit.hasOwnProperty` coming from? Looking at the chain we can see that the method is provided by `Object.prototype.hasOwnProperty`. In other words, it's inherited. + +...But why `hasOwnProperty` does not appear in `for..in` loop, if it lists all inherited properties? The answer is simple: it's not enumerable. Just like all other properties of `Object.prototype`. That's why they are not listed. + +## Summary + +Here's a brief list of methods we discussed in this chapter -- as a recap: + +- [Object.create(proto[, descriptors])](mdn:js/Object/create) -- creates an empty object with given `proto` as `[[Prototype]]` (can be `null`) and optional property descriptors. +- [Object.getPrototypeOf(obj)](mdn:js/Object.getPrototypeOf) -- returns the `[[Prototype]]` of `obj` (same as `__proto__` getter). +- [Object.setPrototypeOf(obj, proto)](mdn:js/Object.setPrototypeOf) -- sets the `[[Prototype]]` of `obj` to `proto` (same as `__proto__` setter). +- [Object.keys(obj)](mdn:js/Object/keys) / [Object.values(obj)](mdn:js/Object/values) / [Object.entries(obj)](mdn:js/Object/entries) -- returns an array of enumerable own string property names/values/key-value pairs. +- [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols) -- returns an array of all own symbolic property names. +- [Object.getOwnPropertyNames(obj)](mdn:js/Object/getOwnPropertyNames) -- returns an array of all own string property names. +- [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) -- returns an array of all own property names. +- [obj.hasOwnProperty(key)](mdn:js/Object/hasOwnProperty): it returns `true` if `obj` has its own (not inherited) property named `key`. + +We also made it clear that `__proto__` is a getter/setter for `[[Prototype]]` and resides in `Object.prototype`, just as other methods. + +We can create an object without a prototype by `Object.create(null)`. Such objects are used as "pure dictionaries", they have no issues with `"__proto__"` as the key. + +All methods that return object properties (like `Object.keys` and others) -- return "own" properties. If we want inherited ones, then we can use `for..in`. diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-2.png b/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-2.png new file mode 100644 index 00000000..ee42f6b9 Binary files /dev/null and b/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-2.png differ diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-2@2x.png b/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-2@2x.png new file mode 100644 index 00000000..1917c663 Binary files /dev/null and b/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-2@2x.png differ diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-null.png b/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-null.png new file mode 100644 index 00000000..7c2e3f9c Binary files /dev/null and b/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-null.png differ diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-null@2x.png b/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-null@2x.png new file mode 100644 index 00000000..fec1facb Binary files /dev/null and b/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-null@2x.png differ diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/rabbit-animal-object.png b/1-js/07-object-oriented-programming/06-prototype-methods/rabbit-animal-object.png new file mode 100644 index 00000000..3254270f Binary files /dev/null and b/1-js/07-object-oriented-programming/06-prototype-methods/rabbit-animal-object.png differ diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/rabbit-animal-object@2x.png b/1-js/07-object-oriented-programming/06-prototype-methods/rabbit-animal-object@2x.png new file mode 100644 index 00000000..f794d7b8 Binary files /dev/null and b/1-js/07-object-oriented-programming/06-prototype-methods/rabbit-animal-object@2x.png differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/1-inheritance-error-assign/solution.md b/1-js/07-object-oriented-programming/08-class-patterns/1-inheritance-error-assign/solution.md new file mode 100644 index 00000000..55f945ca --- /dev/null +++ b/1-js/07-object-oriented-programming/08-class-patterns/1-inheritance-error-assign/solution.md @@ -0,0 +1,46 @@ +Here's the line with the error: + +```js +Rabbit.prototype = Animal.prototype; +``` + +Here `Rabbit.prototype` and `Animal.prototype` become the same object. So methods of both classes become mixed in that object. + +As a result, `Rabbit.prototype.walk` overwrites `Animal.prototype.walk`, so all animals start to bounce: + +```js run +function Animal(name) { + this.name = name; +} + +Animal.prototype.walk = function() { + alert(this.name + ' walks'); +}; + +function Rabbit(name) { + this.name = name; +} + +*!* +Rabbit.prototype = Animal.prototype; +*/!* + +Rabbit.prototype.walk = function() { + alert(this.name + " bounces!"); +}; + +*!* +let animal = new Animal("pig"); +animal.walk(); // pig bounces! +*/!* +``` + +The correct variant would be: + +```js +Rabbit.prototype.__proto__ = Animal.prototype; +// or like this: +Rabbit.prototype = Object.create(Animal.prototype); +``` + +That makes prototypes separate, each of them stores methods of the corresponding class, but `Rabbit.prototype` inherits from `Animal.prototype`. diff --git a/1-js/07-object-oriented-programming/08-class-patterns/1-inheritance-error-assign/task.md b/1-js/07-object-oriented-programming/08-class-patterns/1-inheritance-error-assign/task.md new file mode 100644 index 00000000..ee486c3d --- /dev/null +++ b/1-js/07-object-oriented-programming/08-class-patterns/1-inheritance-error-assign/task.md @@ -0,0 +1,29 @@ +importance: 5 + +--- + +# An error in the inheritance + +Find an error in the prototypal inheritance below. + +What's wrong? What are consequences going to be? + +```js +function Animal(name) { + this.name = name; +} + +Animal.prototype.walk = function() { + alert(this.name + ' walks'); +}; + +function Rabbit(name) { + this.name = name; +} + +Rabbit.prototype = Animal.prototype; + +Rabbit.prototype.walk = function() { + alert(this.name + " bounces!"); +}; +``` diff --git a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.md b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.md new file mode 100644 index 00000000..300b25d9 --- /dev/null +++ b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.md @@ -0,0 +1 @@ +Please note that properties that were internal in functional style (`template`, `timer`) and the internal method `render` are marked private with the underscore `_`. diff --git a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.view/clock.js b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.view/clock.js new file mode 100644 index 00000000..7a193b79 --- /dev/null +++ b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.view/clock.js @@ -0,0 +1,32 @@ +function Clock({ template }) { + this._template = template; +} + +Clock.prototype._render = function() { + let date = new Date(); + + let hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + let mins = date.getMinutes(); + if (mins < 10) min = '0' + mins; + + let secs = date.getSeconds(); + if (secs < 10) secs = '0' + secs; + + let output = this._template + .replace('h', hours) + .replace('m', mins) + .replace('s', secs); + + console.log(output); +}; + +Clock.prototype.stop = function() { + clearInterval(this._timer); +}; + +Clock.prototype.start = function() { + this._render(); + this._timer = setInterval(() => this._render(), 1000); +}; diff --git a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.view/index.html b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.view/index.html new file mode 100644 index 00000000..fdee13d0 --- /dev/null +++ b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.view/index.html @@ -0,0 +1,18 @@ + + + + + Console clock + + + + + + + + + + diff --git a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/source.view/clock.js b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/source.view/clock.js new file mode 100644 index 00000000..26081a35 --- /dev/null +++ b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/source.view/clock.js @@ -0,0 +1,34 @@ +function Clock({ template }) { + + let timer; + + function render() { + let date = new Date(); + + let hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + let mins = date.getMinutes(); + if (mins < 10) min = '0' + mins; + + let secs = date.getSeconds(); + if (secs < 10) secs = '0' + secs; + + let output = template + .replace('h', hours) + .replace('m', mins) + .replace('s', secs); + + console.log(output); + } + + this.stop = function() { + clearInterval(timer); + }; + + this.start = function() { + render(); + timer = setInterval(render, 1000); + }; + +} diff --git a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/source.view/index.html b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/source.view/index.html new file mode 100644 index 00000000..fdee13d0 --- /dev/null +++ b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/source.view/index.html @@ -0,0 +1,18 @@ + + + + + Console clock + + + + + + + + + + diff --git a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/task.md b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/task.md new file mode 100644 index 00000000..71131816 --- /dev/null +++ b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/task.md @@ -0,0 +1,9 @@ +importance: 5 + +--- + +# Rewrite to prototypes + +The `Clock` class is written in functional style. Rewrite it using prototypes. + +P.S. The clock ticks in the console, open it to see. diff --git a/1-js/07-object-oriented-programming/08-class-patterns/article.md b/1-js/07-object-oriented-programming/08-class-patterns/article.md new file mode 100644 index 00000000..83c312a2 --- /dev/null +++ b/1-js/07-object-oriented-programming/08-class-patterns/article.md @@ -0,0 +1,243 @@ + +# Class patterns + +```quote author="Wikipedia" +In object-oriented programming, a *class* is an extensible program-code-template for creating objects, providing initial values for state (member variables) and implementations of behavior (member functions or methods). +``` + +There's a special syntax construct and a keyword `class` in JavaScript. But before studying it, we should consider that the term "class" comes the theory of object-oriented programming. The definition is cited above, and it's language-independant. + +In JavaScript there are several well-known programming patterns to make classes even without using the `class` keyword. And here we'll talk about them first. + +The `class` construct will be described in the next chapter, but in JavaScript it's a "syntax sugar" and an extension of one of the patterns that we'll study here. + +[cut] + + +## Functional class pattern + +The constructor function below can be considered a "class" according to the definition: + +```js run +function User(name) { + this.sayHi = function() { + alert(name); + }; +} + +let user = new User("John"); +user.sayHi(); // John +``` + +It follows all parts of the definition: + +1. It is a "program-code-template" for creating objects (callable with `new`). +2. It provides initial values for the state (`name` from parameters). +3. It provides methods (`sayHi`). + +This is called *functional class pattern*. + +In the functional class pattern, local variables and nested functions inside `User`, that are not assigned to `this`, are visible from inside, but not accessible by the outer code. + +So we can easily add internal functions and variables, like `calcAge()` here: + +```js run +function User(name, birthday) { + +*!* + // only visible from other methods inside User + function calcAge() { + return new Date().getFullYear() - birthday.getFullYear(); + } +*/!* + + this.sayHi = function() { + alert(name + ', age:' + calcAge()); + }; +} + +let user = new User("John", new Date(2000,0,1)); +user.sayHi(); // John +``` + +In this code variables `name`, `birthday` and the function `calcAge()` are internal, *private* to the object. They are only visible from inside of it. + +From the other hand, `sayHi` is the external, *public* method. The external code that creates `user` can access it. + +This way we can hide internal implementation details and helper methods from the outer code. Only what's assigned to `this` becomes visible outside. + +## Factory class pattern + +We can create a class without using `new` at all. + +Like this: + +```js run +function User(name, birthday) { + // only visible from other methods inside User + function calcAge() { + return new Date().getFullYear() - birthday.getFullYear(); + } + + return { + sayHi() { + alert(name + ', age:' + calcAge()); + } + }; +} + +*!* +let user = User("John", new Date(2000,0,1)); +*/!* +user.sayHi(); // John +``` + +As we can see, the function `User` returns an object with public properties and methods. The only benefit of this method is that we can omit `new`: write `let user = User(...)` instead of `let user = new User(...)`. In other aspects it's almost the same as the functional pattern. + +## Prototype-based classes + +Prototype-based classes is the most important and generally the best. Functional and factory class patterns are rarely used in practice. + +Soon you'll see why. + +Here's the same class rewritten using prototypes: + +```js run +function User(name, birthday) { +*!* + this._name = name; + this._birthday = birthday; +*/!* +} + +*!* +User.prototype._calcAge = function() { +*/!* + return new Date().getFullYear() - this._birthday.getFullYear(); +}; + +User.prototype.sayHi = function() { + alert(this._name + ', age:' + this._calcAge()); +}; + +let user = new User("John", new Date(2000,0,1)); +user.sayHi(); // John +``` + +The code structure: + +- The constructor `User` only initializes the current object state. +- Methods are added to `User.prototype`. + +As we can see, methods are lexically not inside `function User`, they do not share a common lexical environment. If we declare variables inside `function User`, then they won't be visible to methods. + +So, there is a widely known agreement that internal properties and methods are prepended with an underscore `"_"`. Like `_name` or `_calcAge()`. Technically, that's just an agreement, the outer code still can access them. But most developers recognize the meaning of `"_"` and try not to touch prefixed properties and methods in the external code. + +Here are the advantages over the functional pattern: + +- In the functional pattern, each object has its own copy of every method. We assign a separate copy of `this.sayHi = function() {...}` and other methods in the constructor. +- In the prototypal pattern, all methods are in `User.prototype` that is shared between all user objects. An object itself only stores the data. + +So the prototypal pattern is more memory-efficient. + +...But not only that. Prototypes allow us to setup the inheritance in a really efficient way. Built-in JavaScript objects all use prototypes. Also there's a special syntax construct: "class" that provides nice-looking syntax for them. And there's more, so let's go on with them. + +## Prototype-based inheritance for classes + +Let's say we have two prototype-based classes. + +`Rabbit`: + +```js +function Rabbit(name) { + this.name = name; +} + +Rabbit.prototype.jump = function() { + alert(this.name + ' jumps!'); +}; + +let rabbit = new Rabbit("My rabbit"); +``` + +![](rabbit-animal-independent-1.png) + +...And `Animal`: + +```js +function Animal(name) { + this.name = name; +} + +Animal.prototype.eat = function() { + alert(this.name + ' eats.'); +}; + +let animal = new Animal("My animal"); +``` + +![](rabbit-animal-independent-2.png) + +Right now they are fully independent. + +But we'd want `Rabbit` to extend `Animal`. In other words, rabbits should be based on animals, have access to methods of `Animal` and extend them with its own methods. + +What does it mean in the language on prototypes? + +Right now methods for `rabbit` objects are in `Rabbit.prototype`. We'd like `rabbit` to use `Animal.prototype` as a "fallback", if the method is not found in `Rabbit.prototype`. + +So the prototype chain should be `rabbit` -> `Rabbit.prototype` -> `Animal.prototype`. + +Like this: + +![](class-inheritance-rabbit-animal.png) + +The code to implement that: + +```js run +// Same Animal as before +function Animal(name) { + this.name = name; +} + +// All animals can eat, right? +Animal.prototype.eat = function() { + alert(this.name + ' eats.'); +}; + +// Same Rabbit as before +function Rabbit(name) { + this.name = name; +} + +Rabbit.prototype.jump = function() { + alert(this.name + ' jumps!'); +}; + +*!* +// setup the inheritance chain +Rabbit.prototype.__proto__ = Animal.prototype; // (*) +*/!* + +let rabbit = new Rabbit("White Rabbit"); +*!* +rabbit.eat(); // rabbits can eat too +*/!* +rabbit.jump(); +``` + +The line `(*)` sets up the prototype chain. So that `rabbit` first searches methods in `Rabbit.prototype`, then `Animal.prototype`. And then, just for completeness, let's mention that if the method is not found in `Animal.prototype`, then the search continues in `Object.prototype`, because `Animal.prototype` is a regular plain object, so it inherits from it. + +So here's the full picture: + +![](class-inheritance-rabbit-animal-2.png) + +## Summary + +The term "class" comes from the object-oriented programming. In JavaScript it usually means the functional class pattern or the prototypal pattern. The prototypal pattern is more powerful and memory-efficient, so it's recommended to stick to it. + +According to the prototypal pattern: +1. Methods are stored in `Class.prototype`. +2. Prototypes inherit from each other. + +In the next chapter we'll study `class` keyword and construct. It allows to write prototypal classes shorter and provides some additional benefits. diff --git a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2.png b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2.png new file mode 100644 index 00000000..ad4a4093 Binary files /dev/null and b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2.png differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2@2x.png b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2@2x.png new file mode 100644 index 00000000..199ed3ee Binary files /dev/null and b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2@2x.png differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal.png b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal.png new file mode 100644 index 00000000..70708c28 Binary files /dev/null and b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal.png differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal@2x.png b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal@2x.png new file mode 100644 index 00000000..0db13018 Binary files /dev/null and b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal@2x.png differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-1.png b/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-1.png new file mode 100644 index 00000000..8df10f5d Binary files /dev/null and b/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-1.png differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-1@2x.png b/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-1@2x.png new file mode 100644 index 00000000..b61ddfbd Binary files /dev/null and b/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-1@2x.png differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-2.png b/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-2.png new file mode 100644 index 00000000..435ec5f8 Binary files /dev/null and b/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-2.png differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-2@2x.png b/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-2@2x.png new file mode 100644 index 00000000..5731da73 Binary files /dev/null and b/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-2@2x.png differ diff --git a/2-ui/3-event-details/8-onscroll/1-avatar-above-scroll/solution.md b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.md similarity index 100% rename from 2-ui/3-event-details/8-onscroll/1-avatar-above-scroll/solution.md rename to 1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.md diff --git a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.view/clock.js b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.view/clock.js new file mode 100644 index 00000000..c710b9da --- /dev/null +++ b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.view/clock.js @@ -0,0 +1,34 @@ +class Clock { + constructor({ template }) { + this._template = template; + } + + _render() { + let date = new Date(); + + let hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + let mins = date.getMinutes(); + if (mins < 10) min = '0' + mins; + + let secs = date.getSeconds(); + if (secs < 10) secs = '0' + secs; + + let output = this._template + .replace('h', hours) + .replace('m', mins) + .replace('s', secs); + + console.log(output); + } + + stop() { + clearInterval(this._timer); + } + + start() { + this._render(); + this._timer = setInterval(() => this._render(), 1000); + } +} diff --git a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.view/index.html b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.view/index.html new file mode 100644 index 00000000..fdee13d0 --- /dev/null +++ b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.view/index.html @@ -0,0 +1,18 @@ + + + + + Console clock + + + + + + + + + + diff --git a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/source.view/clock.js b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/source.view/clock.js new file mode 100644 index 00000000..b1a26250 --- /dev/null +++ b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/source.view/clock.js @@ -0,0 +1,34 @@ + + +function Clock({ template }) { + this._template = template; +} + +Clock.prototype._render = function() { + let date = new Date(); + + let hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + let mins = date.getMinutes(); + if (mins < 10) min = '0' + mins; + + let secs = date.getSeconds(); + if (secs < 10) secs = '0' + secs; + + let output = this._template + .replace('h', hours) + .replace('m', mins) + .replace('s', secs); + + console.log(output); +}; + +Clock.prototype.stop = function() { + clearInterval(this._timer); +}; + +Clock.prototype.start = function() { + this._render(); + this._timer = setInterval(() => this._render(), 1000); +}; diff --git a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/source.view/index.html b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/source.view/index.html new file mode 100644 index 00000000..fdee13d0 --- /dev/null +++ b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/source.view/index.html @@ -0,0 +1,18 @@ + + + + + Console clock + + + + + + + + + + diff --git a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/task.md b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/task.md new file mode 100644 index 00000000..a29d347f --- /dev/null +++ b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/task.md @@ -0,0 +1,9 @@ +importance: 5 + +--- + +# Rewrite to class + +Rewrite the `Clock` class from prototypes to the modern "class" syntax. + +P.S. The clock ticks in the console, open it to see. diff --git a/1-js/07-object-oriented-programming/09-class/animal-rabbit-extends.png b/1-js/07-object-oriented-programming/09-class/animal-rabbit-extends.png new file mode 100644 index 00000000..2db88f36 Binary files /dev/null and b/1-js/07-object-oriented-programming/09-class/animal-rabbit-extends.png differ diff --git a/1-js/07-object-oriented-programming/09-class/animal-rabbit-extends@2x.png b/1-js/07-object-oriented-programming/09-class/animal-rabbit-extends@2x.png new file mode 100644 index 00000000..9539fe9e Binary files /dev/null and b/1-js/07-object-oriented-programming/09-class/animal-rabbit-extends@2x.png differ diff --git a/1-js/07-object-oriented-programming/09-class/article.md b/1-js/07-object-oriented-programming/09-class/article.md new file mode 100644 index 00000000..cb1beec0 --- /dev/null +++ b/1-js/07-object-oriented-programming/09-class/article.md @@ -0,0 +1,310 @@ + +# Classes + +The "class" construct allows to define prototype-based classes with a clean, nice-looking syntax. + +[cut] + +## The "class" syntax + +The `class` syntax is versatile, we'll start from a simple example first. + +Here's a prototype-based class `User`: + +```js run +function User(name) { + this.name = name; +} + +User.prototype.sayHi = function() { + alert(this.name); +} + +let user = new User("John"); +user.sayHi(); +``` + +...And that's the same using `class` syntax: + +```js run +class User { + + constructor(name) { + this.name = name; + } + + sayHi() { + alert(this.name); + } + +} + +let user = new User("John"); +user.sayHi(); +``` + +It's easy to see that the two examples are alike. So, what exactly does `class` do? We may think that it defines a new language-level entity, but that would be wrong. + +The `class User {...}` here actually does two things: + +1. Declares a variable `User` that references the function named `"constructor"`. +2. Puts into `User.prototype` methods listed in the definition. Here it includes `sayHi` and the `constructor`. + +Here's some code to demonstrate that: + +```js run +class User { + constructor(name) { this.name = name; } + sayHi() { alert(this.name); } +} + +*!* +// proof: User is the "constructor" function +*/!* +alert(User == User.prototype.constructor); // true + +*!* +// proof: there are two methods in its "prototype" +*/!* +alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi +``` + +Here's the illustration of `class User`: + +![](class-user.png) + +So `class` is a special syntax to define the constructor with prototype methods. + +...But not only that. There are minor tweaks here and there to ensure the right usage. + +For instance, the `constructor` function can't be called without `new`: +```js run +class User { + constructor() {} +} + +alert(typeof User); // function +User(); // Error: Class constructor User cannot be invoked without 'new' +``` + +```smart header="Outputting a class" +If we output it like `alert(User)`, some engines show `"class User..."`, while others show `"function User..."`. + +Please don't be confused: the string representation may vary, but that's still a function, there is no separate "class" entity in JavaScript language. +``` + +```smart header="Class methods are non-enumerable" +Class definition sets `enumerable` flag to `false` for all methods in the `"prototype"`. That's good, because if we `for..in` over an object, we usually don't want its class methods. +``` + +```smart header="What if there's no constructor?" +If there's no `constructor` in the `class` construct, then an empty function is generated, same as if we had written `constructor() {}`. +``` + +```smart header="Classes always `use strict`" +All code inside the class construct is automatically in strict mode. +``` + +### Getters/setters + +Classes may also include getters/setters. Here's an example with `user.name` implemented using them: + +```js run +class User { + + constructor(name) { + // invokes the setter + this.name = name; + } + +*!* + get name() { +*/!* + return this._name; + } + +*!* + set name(value) { +*/!* + if (value.length < 4) { + alert("Name too short."); + return; + } + this._name = value; + } + +} + +let user = new User("John"); +alert(user.name); // John + +user = new User(""); // Name too short. +``` + +### Only methods + +Unlike object literals, no `property:value` assignments are allowed inside `class`. There may be only methods (without a comma between them) and getters/setters. + +The idea is that everything inside `class` goes to the prototype. And the prototype should store methods only, which are shared between objects. The data describing a concrete object state should reside in individual objects. + +If we really insist on putting a non-function value into the prototype, then `class` can't help here. We can alter `prototype` manually though, like this: + +```js run +class User { } + +User.prototype.test = 5; + +alert( new User().test ); // 5 +``` + +So, technically that's possible, but we should know why we're doing it. + +An alternative here would be to use a getter: + +```js run +class User { + get test() { + return 5; + } +} + +alert( new User().test ); // 5 +``` + +From the external code, the usage is the same. But the getter variant is probably a bit slower. + +## Class Expression + +Just like functions, classes can be defined inside another expression, passed around, returned etc. + +Here's a class-returning function ("class factory"): + +```js run +function getClass(phrase) { +*!* + return class { + sayHi() { + alert(phrase); + }; + }; +*/!* +} + +let User = getClass("Hello"); + +new User().sayHi(); // Hello +``` + +That's quite normal if we recall that `class` is just a special form of function-with-prototype definition. + +And, like Named Function Expressions, such classes also may have a name, that is visible inside that class only: + +```js run +let User = class *!*MyClass*/!* { + sayHi() { + alert(MyClass); + } +}; + +new User().sayHi(); // works, shows MyClass definition + +alert(MyClass); // error, MyClass is only visible in methods of the class +``` + +## Static methods + +Static methods are bound to the class function, not to its `"prototype"`. + +An example: + +```js run +class User { +*!* + static staticMethod() { +*/!* + alert(this == User); + } +} + +User.staticMethod(); // true +``` + +That actually does the same as assigning it as a function property: + +```js +function User() { } + +User.staticMethod = function() { + alert(this == User); +}; +``` + +The value of `this` inside `User.staticMethod()` is the class constructor `User` itself (the "object before dot" rule). + +Usually, static methods are used when the code is related to the class, but not to a particular object of it. + +For instance, we have `Article` objects and need a function to compare them. The natural choice would be `Article.compare`, like this: + +```js run +class Article { + constructor(title, date) { + this.title = title; + this.date = date; + } + +*!* + static compare(articleA, articleB) { + return articleA.date - articleB.date; + } +*/!* +} + +// usage +let articles = [ + new Article("Mind", new Date(2016, 1, 1)), + new Article("Body", new Date(2016, 0, 1)), + new Article("JavaScript", new Date(2016, 11, 1)) +]; + +*!* +articles.sort(Article.compare); +*/!* + +alert( articles[0].title ); // Body +``` + +Here `Article.compare` stands "over" the articles, as a means to compare them. + +Another example would be a so-called "factory" method, that creates an object with specific parameters. + +Like `Article.createTodays()` here: + +```js run +class Article { + constructor(title, date) { + this.title = title; + this.date = date; + } + +*!* + static createTodays() { + // remember, this = Article + return new this("Todays digest", new Date()); + } +*/!* +} + +let article = Article.createTodays(); + +alert( articles.title ); // Todays digest +``` + +Now every time we need to create a todays digest, we can call `Article.createTodays()`. + +Static methods are often used in database-related classes to search/save/remove entries from the database, like this: + +```js +// assuming Article is a special class for managing articles +// static method to remove the article: +Article.remove({id: 12345}); +``` diff --git a/1-js/07-object-oriented-programming/09-class/class-user.png b/1-js/07-object-oriented-programming/09-class/class-user.png new file mode 100644 index 00000000..5579e6bb Binary files /dev/null and b/1-js/07-object-oriented-programming/09-class/class-user.png differ diff --git a/1-js/07-object-oriented-programming/09-class/class-user@2x.png b/1-js/07-object-oriented-programming/09-class/class-user@2x.png new file mode 100644 index 00000000..5a85e658 Binary files /dev/null and b/1-js/07-object-oriented-programming/09-class/class-user@2x.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/1-class-constructor-error/solution.md b/1-js/07-object-oriented-programming/10-class-inheritance/1-class-constructor-error/solution.md new file mode 100644 index 00000000..4711e482 --- /dev/null +++ b/1-js/07-object-oriented-programming/10-class-inheritance/1-class-constructor-error/solution.md @@ -0,0 +1,27 @@ +That's because the child constructor must call `super()`. + +Here's the corrected code: + +```js run +class Animal { + + constructor(name) { + this.name = name; + } + +} + +class Rabbit extends Animal { + constructor(name) { + *!* + super(name); + */!* + this.created = Date.now(); + } +} + +*!* +let rabbit = new Rabbit("White Rabbit"); // ok now +*/!* +alert(rabbit.name); // White Rabbit +``` diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/1-class-constructor-error/task.md b/1-js/07-object-oriented-programming/10-class-inheritance/1-class-constructor-error/task.md new file mode 100644 index 00000000..380a4720 --- /dev/null +++ b/1-js/07-object-oriented-programming/10-class-inheritance/1-class-constructor-error/task.md @@ -0,0 +1,30 @@ +importance: 5 + +--- + +# Error creating an instance + +Here's the code with `Rabbit` extending `Animal`. + +Unfortunately, `Rabbit` objects can't be created. What's wrong? Fix it. +```js run +class Animal { + + constructor(name) { + this.name = name; + } + +} + +class Rabbit extends Animal { + constructor(name) { + this.name = name; + this.created = Date.now(); + } +} + +*!* +let rabbit = new Rabbit("White Rabbit"); // Error: this is not defined +*/!* +alert(rabbit.name); +``` diff --git a/2-ui/4-forms-controls/4-forms-submit/2-form-validation/solution.md b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.md similarity index 100% rename from 2-ui/4-forms-controls/4-forms-submit/2-form-validation/solution.md rename to 1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.md diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/clock.js b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/clock.js new file mode 100644 index 00000000..c710b9da --- /dev/null +++ b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/clock.js @@ -0,0 +1,34 @@ +class Clock { + constructor({ template }) { + this._template = template; + } + + _render() { + let date = new Date(); + + let hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + let mins = date.getMinutes(); + if (mins < 10) min = '0' + mins; + + let secs = date.getSeconds(); + if (secs < 10) secs = '0' + secs; + + let output = this._template + .replace('h', hours) + .replace('m', mins) + .replace('s', secs); + + console.log(output); + } + + stop() { + clearInterval(this._timer); + } + + start() { + this._render(); + this._timer = setInterval(() => this._render(), 1000); + } +} diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js new file mode 100644 index 00000000..4eb12381 --- /dev/null +++ b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js @@ -0,0 +1,12 @@ +class ExtendedClock extends Clock { + constructor(options) { + super(options); + let { precision=1000 } = options; + this._precision = precision; + } + + start() { + this._render(); + this._timer = setInterval(() => this._render(), this._precision); + } +}; diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/index.html b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/index.html new file mode 100644 index 00000000..7ac1db71 --- /dev/null +++ b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/index.html @@ -0,0 +1,23 @@ + + + + + Console clock + + + + + + + + + + + diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/source.view/clock.js b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/source.view/clock.js new file mode 100644 index 00000000..c710b9da --- /dev/null +++ b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/source.view/clock.js @@ -0,0 +1,34 @@ +class Clock { + constructor({ template }) { + this._template = template; + } + + _render() { + let date = new Date(); + + let hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + let mins = date.getMinutes(); + if (mins < 10) min = '0' + mins; + + let secs = date.getSeconds(); + if (secs < 10) secs = '0' + secs; + + let output = this._template + .replace('h', hours) + .replace('m', mins) + .replace('s', secs); + + console.log(output); + } + + stop() { + clearInterval(this._timer); + } + + start() { + this._render(); + this._timer = setInterval(() => this._render(), 1000); + } +} diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/source.view/index.html b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/source.view/index.html new file mode 100644 index 00000000..b48a2a00 --- /dev/null +++ b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/source.view/index.html @@ -0,0 +1,34 @@ + + + + + Console clock + + + + + + + + + + + diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/task.md b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/task.md new file mode 100644 index 00000000..05da4538 --- /dev/null +++ b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/task.md @@ -0,0 +1,12 @@ +importance: 5 + +--- + +# Extended clock + +We've got a `Clock` class. As of now, it prints the time every second. + +Create a new class `ExtendedClock` that inherits from `Clock` and adds the parameter `precision` -- the number of `ms` between "ticks". Should be `1000` (1 second) by default. + +- Your code should be in the file `extended-clock.js` +- Don't modify the original `clock.js`. Extend it. diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/rabbit-extends-object.png b/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/rabbit-extends-object.png new file mode 100644 index 00000000..d4ff37e5 Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/rabbit-extends-object.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/rabbit-extends-object@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/rabbit-extends-object@2x.png new file mode 100644 index 00000000..a54a9d2f Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/rabbit-extends-object@2x.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/solution.md b/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/solution.md new file mode 100644 index 00000000..16a20f2a --- /dev/null +++ b/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/solution.md @@ -0,0 +1,87 @@ +The answer has two parts. + +The first, an easy one is that the inheriting class needs to call `super()` in the constructor. Otherwise `"this"` won't be "defined". + +So here's the fix: + +```js run +class Rabbit extends Object { + constructor(name) { +*!* + super(); // need to call the parent constructor when inheriting +*/!* + this.name = name; + } +} + +let rabbit = new Rabbit("Rab"); + +alert( rabbit.hasOwnProperty('name') ); // true +``` + +But that's not all yet. + +Even after the fix, there's still important difference in `"class Rabbit extends Object"` versus `class Rabbit`. + +As we know, the "extends" syntax sets up two prototypes: + +1. Between `"prototype"` of the constructor functions (for methods). +2. Between the constructor functions itself (for static methods). + +In our case, for `class Rabbit extends Object` it means: + +```js run +class Rabbit extends Object {} + +alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true +alert( Rabbit.__proto__ === Object ); // (2) true +``` + +So we can access static methods of `Object` via `Rabbit`, like this: + +```js run +class Rabbit extends Object {} + +*!* +// normally we call Object.getOwnPropertyNames +alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // a,b +*/!* +``` + +And if we don't use `extends`, then `class Rabbit` does not get the second reference. + +Please compare with it: + +```js run +class Rabbit {} + +alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true +alert( Rabbit.__proto__ === Object ); // (2) false (!) + +*!* +// error, no such function in Rabbit +alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // Error +*/!* +``` + +For the simple `class Rabbit`, the `Rabbit` function has the same prototype + +```js run +class Rabbit {} + +// instead of (2) that's correct for Rabbit (just like any function): +alert( Rabbit.__proto__ === Function.prototype ); +``` + +By the way, `Function.prototype` has "generic" function methods, like `call`, `bind` etc. They are ultimately available in both cases, because for the built-in `Object` constructor, `Object.__proto__ === Function.prototype`. + +Here's the picture: + +![](rabbit-extends-object.png) + +So, to put it short, there are two differences: + +| class Rabbit | class Rabbit extends Object | +|--------------|------------------------------| +| -- | needs to call `super()` in constructor | +| `Rabbit.__proto__ === Function.prototype` | `Rabbit.__proto__ === Object` | diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/task.md b/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/task.md new file mode 100644 index 00000000..d59de085 --- /dev/null +++ b/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/task.md @@ -0,0 +1,43 @@ +importance: 5 + +--- + +# Class extends Object? + +As we know, all objects normally inherit from `Object.prototype` and get access to "generic" object methods. + +Like demonstrated here: + +```js run +class Rabbit { + constructor(name) { + this.name = name; + } +} + +let rabbit = new Rabbit("Rab"); + +*!* +// hasOwnProperty method is from Object.prototype +// rabbit.__proto__ === Object.prototype +alert( rabbit.hasOwnProperty('name') ); // true +*/!* +``` + +So, is it correct to say that `"class Rabbit extends Object"` does exactly the same as `"class Rabbit"`, or not? + +Will it work? + +```js +class Rabbit extends Object { + constructor(name) { + this.name = name; + } +} + +let rabbit = new Rabbit("Rab"); + +alert( rabbit.hasOwnProperty('name') ); // true +``` + +If it won't please fix the code. diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-static.png b/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-static.png new file mode 100644 index 00000000..998c8233 Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-static.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-static@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-static@2x.png new file mode 100644 index 00000000..98a80d38 Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-static@2x.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/article.md b/1-js/07-object-oriented-programming/10-class-inheritance/article.md new file mode 100644 index 00000000..1fe66877 --- /dev/null +++ b/1-js/07-object-oriented-programming/10-class-inheritance/article.md @@ -0,0 +1,574 @@ + +# Class inheritance, super + +Classes can extend one another. There's a nice syntax, technically based on the prototypal inheritance. + +To inherit from another class, we should specify `"extends"` and the parent class before the brackets `{..}`. + +[cut] + +Here `Rabbit` inherits from `Animal`: + +```js run +class Animal { + + constructor(name) { + this.speed = 0; + this.name = name; + } + + run(speed) { + this.speed += speed; + alert(`${this.name} runs with speed ${this.speed}.`); + } + + stop() { + this.speed = 0; + alert(`${this.name} stopped.`); + } + +} + +*!* +// Inherit from Animal +class Rabbit extends Animal { + hide() { + alert(`${this.name} hides!`); + } +} +*/!* + +let rabbit = new Rabbit("White Rabbit"); + +rabbit.run(5); // White Rabbit runs with speed 5. +rabbit.hide(); // White Rabbit hides! +``` + +The `extends` keyword actually adds a `[[Prototype]]` reference from `Rabbit.prototype` to `Animal.prototype`, just as you expect it to be, and as we've seen before. + +![](animal-rabbit-extends.png) + +So now `rabbit` has access both to its own methods and to methods of `Animal`. + +````smart header="Any expression is allowed after `extends`" +Class syntax allows to specify not just a class, but any expression after `extends`. + +For instance, a function call that generates the parent class: + +```js run +function f(phrase) { + return class { + sayHi() { alert(phrase) } + } +} + +*!* +class User extends f("Hello") {} +*/!* + +new User().sayHi(); // Hello +``` +Here `class User` inherits from the result of `f("Hello")`. + +That may be useful for advanced programming patterns when we use functions to generate classes depending on many conditions and can inherit from them. +```` + +## Overriding a method + +Now let's move forward and override a method. As of now, `Rabbit` inherits the `stop` method that sets `this.speed = 0` from `Animal`. + +If we specify our own `stop` in `Rabbit`, then it will be used instead: + +```js +class Rabbit extends Animal { + stop() { + // ...this will be used for rabbit.stop() + } +} +``` + + +...But usually we don't want to totally replace a parent method, but rather to build on top of it, tweak or extend its functionality. We do something in our method, but call the parent method before/after it or in the process. + +Classes provide `"super"` keyword for that. + +- `super.method(...)` to call a parent method. +- `super(...)` to call a parent constructor (inside our constructor only). + +For instance, let our rabbit autohide when stopped: + +```js run +class Animal { + + constructor(name) { + this.speed = 0; + this.name = name; + } + + run(speed) { + this.speed += speed; + alert(`${this.name} runs with speed ${this.speed}.`); + } + + stop() { + this.speed = 0; + alert(`${this.name} stopped.`); + } + +} + +class Rabbit extends Animal { + hide() { + alert(`${this.name} hides!`); + } + +*!* + stop() { + super.stop(); // call parent stop + this.hide(); // and then hide + } +*/!* +} + +let rabbit = new Rabbit("White Rabbit"); + +rabbit.run(5); // White Rabbit runs with speed 5. +rabbit.stop(); // White Rabbit stopped. White rabbit hides! +``` + +Now `Rabbit` has the `stop` method that calls the parent `super.stop()` in the process. + +````smart header="Arrow functions have no `super`" +As was mentioned in the chapter , arrow functions do not have `super`. + +If accessed, it's taken from the outer function. For instance: +```js +class Rabbit extends Animal { + stop() { + setTimeout(() => super.stop(), 1000); // call parent stop after 1sec + } +} +``` + +The `super` in the arrow function is the same as in `stop()`, so it works as intended. If we specified a "regular" function here, there would be an error: + +```js +// Unexpected super +setTimeout(function() { super.stop() }, 1000); +``` +```` + + +## Overriding constructor + +With constructors, things are is a little bit tricky. + +Till now, `Rabbit` did not have its own `constructor`. + +According to the [specification](https://tc39.github.io/ecma262/#sec-runtime-semantics-classdefinitionevaluation), if a class extends another class and has no `constructor`, then the following `constructor` is generated: + +```js +class Rabbit extends Animal { + // generated for extending classes without own constructors +*!* + constructor(...args) { + super(...args); + } +*/!* +} +``` + +As we can see, it basically calls the parent `constructor` passing it all the arguments. That happens if we don't write a constructor of our own. + +Now let's add a custom constructor to `Rabbit`. It will specify the `earLength` in addition to `name`: + +```js run +class Animal { + constructor(name) { + this.speed = 0; + this.name = name; + } + // ... +} + +class Rabbit extends Animal { + +*!* + constructor(name, earLength) { + this.speed = 0; + this.name = name; + this.earLength = earLength; + } +*/!* + + // ... +} + +*!* +// Doesn't work! +let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined. +*/!* +``` + +Whoops! We've got an error. Now we can't create rabbits. What went wrong? + +The short answer is: constructors in inheriting classes must call `super(...)`, and (!) do it before using `this`. + +...But why? What's going on here? Indeed, the requirement seems strange. + +Of course, there's an explanation. Let's get into details, so you'd really understand what's going on. + +In JavaScript, there's a distinction between a "constructor function of an inheriting class" and all others. In an inheriting class, the corresponding constructor function is labelled with a special internal property `[[ConstructorKind]]:"derived"`. + +The difference is: + +- When a normal constructor runs, it creates an empty object as `this` and continues with it. +- But when a derived constructor runs, it doesn't do it. It expects the parent constructor to do this job. + +So if we're making a constructor of our own, then we must call `super`, because otherwise the object with `this` reference to it won't be created. And we'll get an error. + +For `Rabbit` to work, we need to call `super()` before using `this`, like here: + +```js run +class Animal { + + constructor(name) { + this.speed = 0; + this.name = name; + } + + // ... +} + +class Rabbit extends Animal { + + constructor(name, earLength) { +*!* + super(name); +*/!* + this.earLength = earLength; + } + + // ... +} + +*!* +// now fine +let rabbit = new Rabbit("White Rabbit", 10); +alert(rabbit.name); // White Rabbit +alert(rabbit.earLength); // 10 +*/!* +``` + + +## Super: internals, [[HomeObject]] + +Let's get a little deeper under the hood of `super`. We'll see some interesting things by the way. + +First to say, from all that we've learned till now, it's impossible for `super` to work. + +Yeah, indeed, let's ask ourselves, how it could technically work? When an object method runs, it gets the current object as `this`. If we call `super.method()` then, how to retrieve that method? In other words, we need to take the `method` from the parent prototype of the current object. How, technically, we (or a JavaScript engine) can do it? + +Maybe we can get it `[[Prototype]]` of `this`, as `this.__proto__.method`? Unfortunately, that won't work. + +Let's try to do it. Without classes, using plain objects for the sake of simplicity. + +Here, `rabbit.eat()` should call `animal.eat()` method of the parent object: + +```js run +let animal = { + name: "Animal", + eat() { + alert(this.name + " eats."); + } +}; + +let rabbit = { + __proto__: animal, + name: "Rabbit", + eat() { +*!* + this.__proto__.eat.call(this); // (*) +*/!* + } +}; + +rabbit.eat(); // Rabbit eats. +``` + +At the line `(*)` we take `eat` from the prototype (`animal`) and call it in the context of the current object. Please note that `.call(this)` is important here, because a simple `this.__proto__.eat()` would execute parent `eat` in the context of the prototype, not the current object. + +And here it works. + +Now let's add one more object to the chain. We'll see how things break: + +```js run +let animal = { + name: "Animal", + eat() { + alert(this.name + " eats."); + } +}; + +let rabbit = { + __proto__: animal, + eat() { + // ...bounce around rabbit-style and call parent (animal) method + this.__proto__.eat.call(this); // (*) + } +}; + +let longEar = { + __proto__: rabbit, + eat() { + // ...do something with long ears and call parent (rabbit) method + this.__proto__.eat.call(this); // (**) + } +}; + +*!* +longEar.eat(); // Error: Maximum call stack size exceeded +*/!* +``` + +The code doesn't work any more! We can see the error trying to call `longEar.eat()`. + +It may be not that obvious, but if we trace `longEar.eat()` call, then we can see why. In both lines `(*)` and `(**)` the value of `this` is the current object (`longEar`). That's essential: all object methods get the current object as `this`, not a prototype or something. + +So, in both lines `(*)` and `(**)` the value of `this.__proto__` is exactly the same: `rabbit`. They both call `rabbit.eat` without going up the chain. + +In other words: + +1. Inside `longEar.eat()`, we pass the call up to `rabbit.eat` giving it the same `this=longEar`. + ```js + // inside longEar.eat() we have this = longEar + this.__proto__.eat.call(this) // (**) + // becomes + longEar.__proto__.eat.call(this) + // or + rabbit.eat.call(this); + ``` +2. Inside `rabbit.eat`, we want to pass the call even higher in the chain, but `this=longEar`, so `this.__proto__.eat` is `rabbit.eat`! + + ```js + // inside rabbit.eat() we also have this = longEar + this.__proto__.eat.call(this) // (*) + // becomes + longEar.__proto__.eat.call(this) + // or (again) + rabbit.eat.call(this); + ``` + +3. ...So `rabbit.eat` calls itself in the endless loop, because it can't ascend any further. + +![](this-super-loop.png) + +There problem is unsolvable, because `this` must always be the calling object itself, no matter which parent method is called. So its prototype will always be the immediate parent of the object. We can't go up the chain. + +### `[[HomeObject]]` + +To provide the solution, JavaScript adds one more special internal property for functions: `[[HomeObject]]`. + +**When a function is specified as a class or object method, its `[[HomeObject]]` property becomes that object.** + +This actually violates the idea of "unbound" functions, because methods remember their objects. And `[[HomeObject]]` can't be changed, so this bound is forever. So that's a very important change in the language. + +But this change is safe. `[[HomeObject]]` is used only for calling parent methods in `super`, to resolve the prototype. So it doesn't break compatibility. + +Let's see how it works for `super` -- again, using plain objects: + +```js run +let animal = { + name: "Animal", + eat() { // [[HomeObject]] == animal + alert(this.name + " eats."); + } +}; + +let rabbit = { + __proto__: animal, + name: "Rabbit", + eat() { // [[HomeObject]] == rabbit + super.eat(); + } +}; + +let longEar = { + __proto__: rabbit, + name: "Long Ear", + eat() { // [[HomeObject]] == longEar + super.eat(); + } +}; + +*!* +longEar.eat(); // Long Ear eats. +*/!* +``` + +Every method remembers its object in the internal `[[HomeObject]]` property. Then `super` uses it to resolve the parent prototype. + +`[[HomeObject]]` is defined for methods defined both in classes and in plain objects. But for objects, methods must be specified exactly the given way: as `method()`, not as `"method: function()"`. + +In the example below a non-method syntax is used for comparison. `[[HomeObject]]` property is not set and the inheritance doesn't work: + +```js run +let animal = { + eat: function() { // should be the short syntax: eat() {...} + // ... + } +}; + +let rabbit = { + __proto__: animal, + eat: function() { + super.eat(); + } +}; + +*!* +rabbit.eat(); // Error calling super (because there's no [[HomeObject]]) +*/!* +``` + +## Static methods and inheritance + +The `class` syntax supports inheritance for static properties too. + +For instance: + +```js run +class Animal { + + constructor(name, speed) { + this.speed = speed; + this.name = name; + } + + run(speed = 0) { + this.speed += speed; + alert(`${this.name} runs with speed ${this.speed}.`); + } + + static compare(animalA, animalB) { + return animalA.speed - animalB.speed; + } + +} + +// Inherit from Animal +class Rabbit extends Animal { + hide() { + alert(`${this.name} hides!`); + } +} + +let rabbits = [ + new Rabbit("White Rabbit", 10), + new Rabbit("Black Rabbit", 5) +]; + +rabbits.sort(Rabbit.compare); + +rabbits[0].run(); // Black Rabbit runs with speed 5. +``` + +Now we can call `Rabbit.compare` assuming that the inherited `Animal.compare` will be called. + +How does it work? Again, using prototypes. As you might have already guessed, extends also gives `Rabbit` the `[[Prototype]]` reference to `Animal`. + + +![](animal-rabbit-static.png) + +So, `Rabbit` function now inherits from `Animal` function. And `Animal` function normally has `[[Prototype]]` referencing `Function.prototype`, because it doesn't `extend` anything. + +Here, let's check that: + +```js run +class Animal {} +class Rabbit extends Animal {} + +// for static propertites and methods +alert(Rabbit.__proto__ == Animal); // true + +// and the next step is Function.prototype +alert(Animal.__proto__ == Function.prototype); // true + +// that's in addition to the "normal" prototype chain for object methods +alert(Rabbit.prototype.__proto__ === Animal.prototype); +``` + +This way `Rabbit` has access to all static methods of `Animal`. + +Please note that built-in classes don't have such static `[[Prototype]]` reference. For instance, `Object` has `Object.defineProperty`, `Object.keys` and so on, but `Array`, `Date` etc do not inherit them. + +Here's the picture structure for `Date` and `Object`: + +![](object-date-inheritance.png) + +Note, there's no link between `Date` and `Object`. Both `Object` and `Date` exist independently. `Date.prototype` inherits from `Object.prototype`, but that's all. + +Such difference exists for historical reasons: there was no thought about class syntax and inheriting static methods at the dawn of JavaScript language. + +## Natives are extendable + +Built-in classes like Array, Map and others are extendable also. + +For instance, here `PowerArray` inherits from the native `Array`: + +```js run +// add one more method to it (can do more) +class PowerArray extends Array { + isEmpty() { + return this.length == 0; + } +} + +let arr = new PowerArray(1, 2, 5, 10, 50); +alert(arr.isEmpty()); // false + +let filteredArr = arr.filter(item => item >= 10); +alert(filteredArr); // 10, 50 +alert(filteredArr.isEmpty()); // false +``` + +Please note one very interesting thing. Built-in methods like `filter`, `map` and others -- return new objects of exactly the inherited type. They rely on the `constructor` property to do so. + +In the example above, +```js +arr.constructor === PowerArray +``` + +So when `arr.filter()` is called, it internally creates the new array of results exactly as `new PowerArray`. And we can keep using its methods further down the chain. + +Even more, we can customize that behavior. The static getter `Symbol.species`, if exists, returns the constructor to use in such cases. + +For example, here due to `Symbol.species` built-in methods like `map`, `filter` will return "normal" arrays: + +```js run +class PowerArray extends Array { + isEmpty() { + return this.length == 0; + } + +*!* + // built-in methods will use this as the constructor + static get [Symbol.species]() { + return Array; + } +*/!* +} + +let arr = new PowerArray(1, 2, 5, 10, 50); +alert(arr.isEmpty()); // false + +// filter creates new array using arr.constructor[Symbol.species] as constructor +let filteredArr = arr.filter(item => item >= 10); + +*!* +// filteredArr is not PowerArray, but Array +*/!* +alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function +``` + +We can use it in more advanced keys to strip extended functionality from resulting values if not needed. Or, maybe, to extend it even further. diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-array-object.png b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-array-object.png new file mode 100644 index 00000000..c5d71263 Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-array-object.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-array-object@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-array-object@2x.png new file mode 100644 index 00000000..edc4e841 Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-array-object@2x.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-animal.png b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-animal.png new file mode 100644 index 00000000..70708c28 Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-animal.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-animal@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-animal@2x.png new file mode 100644 index 00000000..0db13018 Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-animal@2x.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-run-animal.png b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-run-animal.png new file mode 100644 index 00000000..387975a9 Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-run-animal.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-run-animal@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-run-animal@2x.png new file mode 100644 index 00000000..ca731359 Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-run-animal@2x.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/object-date-inheritance.png b/1-js/07-object-oriented-programming/10-class-inheritance/object-date-inheritance.png new file mode 100644 index 00000000..542a0c9f Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/object-date-inheritance.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/object-date-inheritance@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/object-date-inheritance@2x.png new file mode 100644 index 00000000..21485062 Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/object-date-inheritance@2x.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/this-super-loop.png b/1-js/07-object-oriented-programming/10-class-inheritance/this-super-loop.png new file mode 100644 index 00000000..bb4c1fc4 Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/this-super-loop.png differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/this-super-loop@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/this-super-loop@2x.png new file mode 100644 index 00000000..91d2e03c Binary files /dev/null and b/1-js/07-object-oriented-programming/10-class-inheritance/this-super-loop@2x.png differ diff --git a/1-js/07-object-oriented-programming/11-instanceof/1-strange-instanceof/solution.md b/1-js/07-object-oriented-programming/11-instanceof/1-strange-instanceof/solution.md new file mode 100644 index 00000000..d41d90ed --- /dev/null +++ b/1-js/07-object-oriented-programming/11-instanceof/1-strange-instanceof/solution.md @@ -0,0 +1,7 @@ +Yeah, looks strange indeed. + +But `instanceof` does not care about the function, but rather about its `prototype`, that it matches against the prototype chain. + +And here `a.__proto__ == B.prototype`, so `instanceof` returns `true`. + +So, by the logic of `instanceof`, the `prototype` actually defines the type, not the constructor function. diff --git a/1-js/07-object-oriented-programming/11-instanceof/1-strange-instanceof/task.md b/1-js/07-object-oriented-programming/11-instanceof/1-strange-instanceof/task.md new file mode 100644 index 00000000..e9481912 --- /dev/null +++ b/1-js/07-object-oriented-programming/11-instanceof/1-strange-instanceof/task.md @@ -0,0 +1,20 @@ +importance: 5 + +--- + +# Strange instanceof + +Why `instanceof` below returns `true`? We can easily see that `a` is not created by `B()`. + +```js run +function A() {} +function B() {} + +A.prototype = B.prototype = {}; + +let a = new A(); + +*!* +alert( a instanceof B ); // true +*/!* +``` diff --git a/1-js/07-object-oriented-programming/11-instanceof/article.md b/1-js/07-object-oriented-programming/11-instanceof/article.md new file mode 100644 index 00000000..1d725eea --- /dev/null +++ b/1-js/07-object-oriented-programming/11-instanceof/article.md @@ -0,0 +1,213 @@ +# Class checking: "instanceof" + +The `instanceof` operator allows to check whether an object belongs to a certain class. It also takes inheritance into account. + +Such a check may be necessary in many cases, here we'll use it for building a *polymorphic* function, the one that treats arguments differently depending on their type. + +[cut] + +## The instanceof operator [#ref-instanceof] + +The syntax is: +```js +obj instanceof Class +``` + +It returns `true` if `obj` belongs to the `Class` (or a class inheriting from it). + +For instance: + +```js run +class Rabbit {} +let rabbit = new Rabbit(); + +// is it an object of Rabbit class? +*!* +alert( rabbit instanceof Rabbit ); // true +*/!* +``` + +It also works with constructor functions: + +```js run +*!* +// instead of class +function Rabbit() {} +*/!* + +alert( new Rabbit() instanceof Rabbit ); // true +``` + +...And with built-in classes like `Array`: + +```js run +let arr = [1, 2, 3]; +alert( arr instanceof Array ); // true +alert( arr instanceof Object ); // true +``` + +Please note that `arr` also belongs to the `Object` class. That's because `Array` prototypally inherits from `Object`. + +The `instanceof` operator examines the prototype chain for the check, and is also fine-tunable using the static method `Symbol.hasInstance`. + +The algorithm of `obj instanceof Class` works roughly as follows: + +1. If there's a static method `Symbol.hasInstance`, then use it. Like this: + + ```js run + // assume anything that canEat is an animal + class Animal { + static [Symbol.hasInstance](obj) { + if (obj.canEat) return true; + } + } + + let obj = { canEat: true }; + alert(obj instanceof Animal); // true: Animal[Symbol.hasInstance](obj) is called + ``` + +2. Most classes do not have `Symbol.hasInstance`. In that case, check if `Class.prototype` equals to one of prototypes in the `obj` prototype chain. + + In other words, compare: + ```js + obj.__proto__ == Class.prototype + obj.__proto__.__proto__ == Class.prototype + obj.__proto__.__proto__.__proto__ == Class.prototype + ... + ``` + + In the example above `Rabbit.prototype == rabbit.__proto__`, so that gives the answer immediately. + + In the case of an inheritance, `rabbit` is an instance of the parent class as well: + + ```js run + class Animal {} + class Rabbit extends Animal {} + + let rabbit = new Rabbit(); + *!* + alert(rabbit instanceof Animal); // true + */!* + // rabbit.__proto__ == Rabbit.prototype + // rabbit.__proto__.__proto__ == Animal.prototype (match!) + ``` + +Here's the illustration of what `rabbit instanceof Animal` compares with `Animal.prototype`: + +![](instanceof.png) + +By the way, there's also a method [objA.isPrototypeOf(objB)](mdn:js/object/isPrototypeOf), that returns `true` if `objA` is somewhere in the chain of prototypes for `objB`. So the test of `obj instanceof Class` can be rephrased as `Class.prototype.isPrototypeOf(obj)`. + +That's funny, but the `Class` constructor itself does not participate in the check! Only the chain of prototypes and `Class.prototype` matters. + +That can lead to interesting consequences when `prototype` is changed. + +Like here: + +```js run +function Rabbit() {} +let rabbit = new Rabbit(); + +// changed the prototype +Rabbit.prototype = {}; + +// ...not a rabbit any more! +*!* +alert( rabbit instanceof Rabbit ); // false +*/!* +``` + +That's one of reasons to avoid changing `prototype`. Just to keep safe. + +## Bonus: Object toString for the type + +We already know that plain objects are converted to string as `[object Object]`: + +```js run +let obj = {}; + +alert(obj); // [object Object] +alert(obj.toString()); // the same +``` + +That's their implementation of `toString`. But there's a hidden feature thank makes `toString` actually much more powerful than that. We can use it as an extended `typeof` and an alternative for `instanceof`. + +Sounds strange? Indeed. Let's demistify. + +By [specification](https://tc39.github.io/ecma262/#sec-object.prototype.tostring), the built-in `toString` can be extracted from the object and executed in the context of any other value. And its result depends on that value. + +- For a number, it will be `[object Number]` +- For a boolean, it will be `[object Boolean]` +- For `null`: `[object Null]` +- For `undefined`: `[object Undefined]` +- For arrays: `[object Array]` +- ...etc (customizable). + +Let's demonstrate: + +```js run +// copy toString method into a variable for convenience +let objectToString = Object.prototype.toString; + +// what type is this? +let arr = []; + +alert( objectToString.call(arr) ); // [object Array] +``` + +Here we used [call](mdn:js/function/call) as described in the chapter [](info:call-apply-decorators) to execute the function `objectToString` in the context `this=arr`. + +Internally, the `toString` algorithm examines `this` and returns the corresponding result. More examples: + +```js run +let s = Object.prototype.toString; + +alert( s.call(123) ); // [object Number] +alert( s.call(null) ); // [object Null] +alert( s.call(alert) ); // [object Function] +``` + +### Symbol.toStringTag + +The behavior of Object `toString` can be customized using a special object property `Symbol.toStringTag`. + +For instance: + +```js run +let user = { + [Symbol.toStringTag]: 'User' +}; + +alert( {}.toString.call(user) ); // [object User] +``` + +For most environment-specific objects, there is such a property. Here are few browser specific examples: + +```js run +// toStringTag for the envinronment-specific object and class: +alert( window[Symbol.toStringTag]); // window +alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest + +alert( {}.toString.call(window) ); // [object Window] +alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest] +``` + +As you can see, the result is exactly `Symbol.toStringTag` (if exists), wrapped into `[object ...]`. + +At the end we have "typeof on steroids" that not only works for primitive data types, but also for built-in objects and even can be customized. + +It can be used instead of `instanceof` for built-in objects when we want to get the type as a string rather than just to check. + +## Summary + +Let's recap the type-checking methods that we know: + +| | works for | returns | +|---------------|-------------|---------------| +| `typeof` | primitives | string | +| `{}.toString` | primitives, built-in objects, objects with `Symbol.toStringTag` | string | +| `instanceof` | objects | true/false | + +As we can see, `{}.toString` is technically a "more advanced" `typeof`. + +And `instanceof` operator really shines when we are working with a class hierarchy and want to check for the class taking into account inheritance. diff --git a/1-js/07-object-oriented-programming/11-instanceof/instanceof.png b/1-js/07-object-oriented-programming/11-instanceof/instanceof.png new file mode 100644 index 00000000..85aa9a55 Binary files /dev/null and b/1-js/07-object-oriented-programming/11-instanceof/instanceof.png differ diff --git a/1-js/07-object-oriented-programming/11-instanceof/instanceof@2x.png b/1-js/07-object-oriented-programming/11-instanceof/instanceof@2x.png new file mode 100644 index 00000000..fba77122 Binary files /dev/null and b/1-js/07-object-oriented-programming/11-instanceof/instanceof@2x.png differ diff --git a/1-js/07-object-oriented-programming/13-mixins/article.md b/1-js/07-object-oriented-programming/13-mixins/article.md new file mode 100644 index 00000000..624f4426 --- /dev/null +++ b/1-js/07-object-oriented-programming/13-mixins/article.md @@ -0,0 +1,199 @@ +# Mixins + +In JavaScript we can only inherit from a single object. There can be only one `[[Prototype]]`. + +But sometimes we need such kind of thing. For instance, we have a code that implements events exchange or templating, and we'd like to be able to add these capabilities to any class easily. + +What can help here is *mixins*. + +As [defined in Wikipedia](https://en.wikipedia.org/wiki/Mixin), a *mixin* is a class that contains methods for use by other classes without having to be the parent class of those other classes. + +In other words, a *mixin* is a class that implements a certain behavior. But we do not use it alone, we use it to add the behavior to other classes. + +## A mixin example + +The simplest way to make a mixin in JavaScript -- is to make an object with useful methods, that we can just copy into the prototype. + +For instance here the mixin `sayHiMixin` is used to add some "speech" for `User`: + +```js run +*!* +// mixin +*/!* +let sayHiMixin = { + sayHi() { + alert("Hello " + this.name); + }, + sayBye() { + alert("Bye " + this.name); + } +}; + +*!* +// usage: +*/!* +class User { + constructor(name) { + this.name = name; + } +} + +// copy the methods +Object.assign(User.prototype, sayHiMixin); + +// now User can say hi +new User("Dude").sayHi(); // Hi Dude! +``` + +There's no inheritance, there's a simple method copying. So `User` may extend some other class and also include the mixin to "mix-in" the additional methods. + +Mixins also can make use of inheritance. + +For instance, here `sayHiMixin` inherits from `sayMixin`: + +```js run +let sayMixin = { + say(phrase) { + alert(phrase); + } +}; + +let sayHiMixin = { + // can use any way of prototype setting here + __proto__: sayMixin, + + sayHi() { + *!* + // call parent method + */!* + super.say("Hello " + this.name); + }, + sayBye() { + super.say("Bye " + this.name); + } +}; + +class User { + constructor(name) { + this.name = name; + } +} + +// copy the methods +Object.assign(User.prototype, sayHiMixin); + +// now User can say hi +new User("Dude").sayHi(); // Hi Dude! +``` + +Please note that the call to the parent method `super.say()` from `sayHiMixin` looks for the method in the prototype of that mixin, not the class. + +![](mixin-inheritance.png) + +That's because methods from `sayHiMixin` have `[[HomeObject]]` set to it. So `super` actually means `sayHiMixin.__proto__`, not `User.__proto__`. + +## EventMixin + +Now a mixin for the real life. + +The important feature of many objects is working with events. + +That is: an object should have a method to "generate an event" when something important happens to it, and other objects should be able to "subscribe" to receive such notifications. + +An event must have a name and, if necessary, the attached data. + +For instance, an object `user` can generate an event `"login"` when the visitor logs in. And an object `calendar` may want to receive such notifications and load the information about that visitor. + +Or, the object `menu` can generate the event `"select"` when a menu item is selected, and other objects may want to get that information and react on that event. + +Events is a way to "share information" with anyone who wants it. + +Here is `eventMixin` that implements the corresponding methods: + +```js run +let eventMixin = { + + /** + * Subscribe to event, usage: + * menu.on('select', function(item) { ... } + */ + on(eventName, handler) { + if (!this._eventHandlers) this._eventHandlers = {}; + if (!this._eventHandlers[eventName]) { + this._eventHandlers[eventName] = []; + } + this._eventHandlers[eventName].push(handler); + }, + + /** + * Cancel the subscription, usage: + * menu.off('select', handler) + */ + off(eventName, handler) { + let handlers = this._eventHandlers && this._eventHandlers[eventName]; + if (!handlers) return; + for(let i = 0; i < handlers.length; i++) { + if (handlers[i] == handler) { + handlers.splice(i--, 1); + } + } + }, + + /** + * Generate the event and attach the data to it + * this.trigger('select', data1, data2); + */ + trigger(eventName, ...args) { + if (!this._eventHandlers || !this._eventHandlers[eventName]) { + return; // no handlers for that event name + } + + // call the handlers + this._eventHandlers[eventName].forEach(handler => handler.apply(this, args)); + } +}; +``` + +There are 3 methods here: + +1. `.on(eventName, handler)` -- assigns function `handler` to run when the event with that name happens. The handlers are stored in the `_eventHandlers` property. +2. `.off(eventName, handler)` -- removes the function from the handlers list. +3. `.trigger(eventName, ...args)` -- generates the event: all assigned handlers are called and `args` are passed as arguments to them. + + +Usage: + +```js run +// Make a class +class Menu { + choose(value) { + this.trigger("select", value); + } +} +// Add the mixin +Object.assign(Menu.prototype, eventMixin); + +let menu = new Menu(); + +// call the handler on selection: +*!* +menu.on("select", value => alert("Value selected: " + value)); +*/!* + +// triggers the event => shows Value selected: 123 +menu.choose("123"); // value selected +``` + +Now if we have the code interested to react on user selection, we can bind it with `menu.on(...)`. + +And the `eventMixin` can add such behavior to as many classes as we'd like, without interfering with the inheritance chain. + +## Summary + +*Mixin* -- is a generic object-oriented programming term: a class that contains methods for other classes. + +Some other languages like e.g. python allow to create mixins using multiple inheritance. JavaScript does not support multiple inheritance, but mixins can be implemented by copying them into the prototype. + +We can use mixins as a way to augment a class by multiple behaviors, like event-handling as we have seen above. + +Mixins may become a point of conflict if they occasionally overwrite native class methods. So generally one should think well about the naming for a mixin, to minimize such possibility. diff --git a/1-js/07-object-oriented-programming/13-mixins/head.html b/1-js/07-object-oriented-programming/13-mixins/head.html new file mode 100644 index 00000000..77ea38b2 --- /dev/null +++ b/1-js/07-object-oriented-programming/13-mixins/head.html @@ -0,0 +1,43 @@ + diff --git a/1-js/07-object-oriented-programming/13-mixins/mixin-inheritance.png b/1-js/07-object-oriented-programming/13-mixins/mixin-inheritance.png new file mode 100644 index 00000000..7cc65503 Binary files /dev/null and b/1-js/07-object-oriented-programming/13-mixins/mixin-inheritance.png differ diff --git a/1-js/07-object-oriented-programming/13-mixins/mixin-inheritance@2x.png b/1-js/07-object-oriented-programming/13-mixins/mixin-inheritance@2x.png new file mode 100644 index 00000000..f53ecf68 Binary files /dev/null and b/1-js/07-object-oriented-programming/13-mixins/mixin-inheritance@2x.png differ diff --git a/1-js/07-object-oriented-programming/index.md b/1-js/07-object-oriented-programming/index.md new file mode 100644 index 00000000..7053ada8 --- /dev/null +++ b/1-js/07-object-oriented-programming/index.md @@ -0,0 +1,3 @@ +# Objects, classes, inheritance + +In this section we return to objects and learn them even more in-depth. diff --git a/1-js/08-error-handling/1-try-catch/1-finally-or-code-after/solution.md b/1-js/08-error-handling/1-try-catch/1-finally-or-code-after/solution.md new file mode 100644 index 00000000..05ba72e0 --- /dev/null +++ b/1-js/08-error-handling/1-try-catch/1-finally-or-code-after/solution.md @@ -0,0 +1,47 @@ +The difference becomes obvious when we look at the code inside a function. + +The behavior is different if there's a "jump out" of `try..catch`. + +For instance, when there's a `return` inside `try..catch`. The `finally` clause works in case of *any* exit from `try..catch`, even via the `return` statement: right after `try..catch` is done, but before the calling code gets the control. + +```js run +function f() { + try { + alert('start'); +*!* + return "result"; +*/!* + } catch (e) { + /// ... + } finally { + alert('cleanup!'); + } +} + +f(); // cleanup! +``` + +...Or when there's a `throw`, like here: + +```js run +function f() { + try { + alert('start'); + throw new Error("an error"); + } catch (e) { + // ... + if("can't handle the error") { +*!* + throw e; +*/!* + } + + } finally { + alert('cleanup!') + } +} + +f(); // cleanup! +``` + +It's `finally` that guarantees the cleanup here. If we just put the code at the end of `f`, it wouldn't run. diff --git a/1-js/08-error-handling/1-try-catch/1-finally-or-code-after/task.md b/1-js/08-error-handling/1-try-catch/1-finally-or-code-after/task.md new file mode 100644 index 00000000..e8468734 --- /dev/null +++ b/1-js/08-error-handling/1-try-catch/1-finally-or-code-after/task.md @@ -0,0 +1,38 @@ +importance: 5 + +--- + +# Finally or just the code? + +Compare the two code fragments. + +1. The first one uses `finally` to execute the code after `try..catch`: + + ```js + try { + work work + } catch (e) { + handle errors + } finally { + *!* + cleanup the working space + */!* + } + ``` +2. The second fragment puts the cleaning right after `try..catch`: + + ```js + try { + work work + } catch (e) { + handle errors + } + + *!* + cleanup the working space + */!* + ``` + +We definitely need the cleanup after the work has started, doesn't matter if there was an error or not. + +Is there an advantage here in using `finally` or both code fragments are equal? If there is such an advantage, then give an example when it matters. diff --git a/1-js/08-error-handling/1-try-catch/article.md b/1-js/08-error-handling/1-try-catch/article.md new file mode 100644 index 00000000..bc3b509b --- /dev/null +++ b/1-js/08-error-handling/1-try-catch/article.md @@ -0,0 +1,662 @@ +# Error handling, "try..catch" + +No matter how great we are at programming, sometimes our scripts have errors. They may occur because of our mistakes, an unexpected user input, an erroneous server response and for a thousand of other reasons. + +Usually, a script "dies" (immediately stops) in case of an error, printing it to console. + +But there's a syntax construct `try..catch` that allows to "catch" errors and, instead of dying, do something more reasonable. + +[cut] + +## The "try..catch" syntax + +The `try..catch` construct has two main blocks: `try`, and then `catch`: + +```js +try { + + // code... + +} catch (err) { + + // error handling + +} +``` + +It works like this: + +1. First, the code in `try {...}` is executed. +2. If there were no errors, then `catch(err)` is ignored: the execution reaches the end of `try` and then jumps over `catch`. +3. If an error occurs, then `try` execution is stopped, and the control flows to the beginning of `catch(err)`. The `err` variable (can use any name for it) contains an error object with details about what's happened. + +![](try-catch-flow.png) + +So, an error inside the `try {…}` block does not kill the script: we have a chance to handle it in `catch`. + +Let's see more examples. + +- An errorless example: shows `alert` `(1)` and `(2)`: + + ```js run + try { + + alert('Start of try runs'); // *!*(1) <--*/!* + + // ...no errors here + + alert('End of try runs'); // *!*(2) <--*/!* + + } catch(err) { + + alert('Catch is ignored, because there are no errors'); // (3) + + } + + alert("...Then the execution continues"); + ``` +- An example with an error: shows `(1)` and `(3)`: + + ```js run + try { + + alert('Start of try runs'); // *!*(1) <--*/!* + + *!* + lalala; // error, variable is not defined! + */!* + + alert('End of try (never reached)'); // (2) + + } catch(err) { + + alert(`Error has occured!`); // *!*(3) <--*/!* + + } + + alert("...Then the execution continues"); + ``` + + +````warn header="`try..catch` only works for runtime errors" +For `try..catch` to work, the code must be runnable. In other words, it should be valid JavaScript. + +It won't work if the code is syntactically wrong, for instance it has unmatched figure brackets: + +```js run +try { + {{{{{{{{{{{{ +} catch(e) { + alert("The engine can't understand this code, it's invalid"); +} +``` + +The JavaScript engine first reads the code, and then runs it. The errors that occur on the reading phrase are called "parse-time" errors and are unrecoverable (from inside that code). That's because the engine can't understand the code. + +So, `try..catch` can only handle errors that occur in the valid code. Such errors are called "runtime errors" or, sometimes, "exceptions". +```` + + +````warn header="`try..catch` works synchronously" +If an exception happens in a "scheduled" code, like in `setTimeout`, then `try..catch` won't catch it: + +```js run +try { + setTimeout(function() { + noSuchVariable; // script will die here + }, 1000); +} catch (e) { + alert( "won't work" ); +} +``` + +That's because `try..catch` actually wraps the `setTimeout` call that schedules the function. But the function itself is executed later, when the engine has already have left the `try..catch` construct. + +To catch an exception inside a scheduled function, `try..catch` must be inside that function: +```js run +setTimeout(function() { + try { + noSuchVariable; // try..catch handles the error! + } catch (e) { + alert( "error is caught here!" ); + } +}, 1000); +``` +```` + +## Error object + +When an error occurs, JavaScript generates an object containing the details about it. The object is then passed as an argument to `catch`: + +```js +try { + // ... +} catch(err) { // <-- the "error object", could use another word instead of err + // ... +} +``` + +For all built-in errors, the error object inside `catch` block has two main properties: + +`name` +: Error name. For an undefined variable that's `"ReferenceError"`. + +`message` +: Textual message about error details. + +There are other non-standard properties available in most environments. One of most widely used and supported is: + +`stack` +: Current call stack: a string with information about the sequence of nested calls that led to the error. Used for debugging purposes. + +For instance: + +```js run untrusted +try { +*!* + lalala; // error, variable is not defined! +*/!* +} catch(err) { + alert(err.name); // ReferenceError + alert(err.message); // lalala is not defined + alert(err.stack); // ReferenceError: lalala is not defined at ... + + // Can also show an error as a whole + // The error is converted to string as "name: message" + alert(err); // ReferenceError: lalala is not defined +} +``` + + +## Using "try..catch" + +Let's explore a real-life use case of `try..catch`. + +As we already know, JavaScript supports method [JSON.parse(str)](mdn:js/JSON/parse) to read JSON-encoded values. + +Usually it's used to decode the data received over the network, from the server or another source. + +We receive it and call `JSON.parse`, like this: + +```js run +let json = '{"name":"John", "age": 30}'; // data from the server + +*!* +let user = JSON.parse(json); // convert the text representation to JS object +*/!* + +// now user is an object with properties from the string +alert( user.name ); // John +alert( user.age ); // 30 +``` + +More detailed information about JSON you can find in the chapter . + +**If `json` is malformed, `JSON.parse` generates an error, so the script "dies".** + +Should we be satisfied with that? Of course, not! + +This way if something's wrong with the data, the visitor will never know that (unless he opens developer console). And people really really don't like when something "just dies" without any error message. + +Let's use `try..catch` to handle the error: + +```js run +let json = "{ bad json }"; + +try { + +*!* + let user = JSON.parse(json); // <-- when an error occurs... +*/!* + alert( user.name ); // doesn't work + +} catch (e) { +*!* + // ...the execution jumps here + alert( "Our apologies, the data has errors, we'll try to request it one more time." ); + alert( e.name ); + alert( e.message ); +*/!* +} +``` + +Here we use `catch` block only to show the message, but we can do much more: a new network request, suggest an alternative to the visitor, send the information about the error to a logging facility... All much better than just dying. + +## Throwing our own errors + +What if `json` is syntactically correct... But doesn't have a required `"name"` property? + +Like this: + +```js run +let json = '{ "age": 30 }'; // incomplete data + +try { + + let user = JSON.parse(json); // <-- no errors +*!* + alert( user.name ); // no name! +*/!* + +} catch (e) { + alert( "doesn't execute" ); +} +``` + +Here `JSON.parse` runs normally, but the absence of `"name"` is actually an error for us. + +To unify error handling, we'll use the `throw` operator. + +### "Throw" operator + +The `throw` operator generates an error. + +The syntax is: + +```js +throw +``` + +Technically, we can use anything as an error object. That may be even a primitive, like a number or a string, but it's better to use objects, preferrably with `name` and `message` properties (to stay somewhat compatible with built-in errors). + +JavaScript has many built-in constructors for standard errors: `Error`, `SyntaxError`, `ReferenceError`, `TypeError` and others. We can use them to create error objects as well. + +Their syntax is: + +```js +let error = new Error(message); +// or +let error = new SyntaxError(message); +let error = new ReferenceError(message); +// ... +``` + +For built-in errors (not for any objects, just for errors), the `name` property is exactly the name of the constructor. And `message` is taken from the argument. + +For instance: + +```js run +let error = new Error("Things happen o_O"); + +alert(error.name); // Error +alert(error.message); // Things happen o_O +``` + +Let's see what kind of error `JSON.parse` generates: + +```js run +try { + JSON.parse("{ bad json o_O }"); +} catch(e) { +*!* + alert(e.name); // SyntaxError +*/!* + alert(e.message); // Unexpected token o in JSON at position 0 +} +``` + +As we can see, that's a `SyntaxError`. + +...And in our case, the absense of `name` could be treated as a syntax error also, assuming that users must have a `"name"`. + +So let's throw it: + +```js run +let json = '{ "age": 30 }'; // incomplete data + +try { + + let user = JSON.parse(json); // <-- no errors + + if (!user.name) { +*!* + throw new SyntaxError("Incomplete data: no name"); // (*) +*/!* + } + + alert( user.name ); + +} catch(e) { + alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name +} +``` + +In the line `(*)` the `throw` operator generates `SyntaxError` with the given `message`, the same way as JavaScript would generate itself. The execution of `try` immediately stops and the control flow jumps into `catch`. + +Now `catch` became a single place for all error handling: both for `JSON.parse` and other cases. + +## Rethrowing + +In the example above we use `try..catch` to handle incorrect data. But is it possible that *another unexpected error* occurs within the `try {...}` block? Like a variable is undefined or something else, not just that "incorrect data" thing. + +Like this: + +```js run +let json = '{ "age": 30 }'; // incomplete data + +try { + user = JSON.parse(json); // <-- forgot to put "let" before user + + // ... +} catch(err) { + alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined + // (not JSON Error actually) +} +``` + +Of course, everything's possible! Programmers do make mistakes. Even in open-source utilities used by millions for decades -- suddenly a crazy bug may be discovered that leads to terrible hacks (like it happened with the `ssh` tool). + +In our case, `try..catch` is meant to catch "incorrect data" errors. But by its nature, `catch` gets *all* errors from `try`. Here it gets an unexpected error, but still shows the same `"JSON Error"` message. That's wrong and also makes the code more difficult to debug. + +Fortunately, we can find out which error we get, for instance from its `name`: + +```js run +try { + user = { /*...*/ }; +} catch(e) { +*!* + alert(e.name); // "ReferenceError" for accessing an undefined variable +*/!* +} +``` + +The rule is simple: + +**Catch should only process errors that it knows and "rethrow" all others.** + +The "rethrowing" technique can be explained in more detail as: + +1. Catch gets all errors. +2. In `catch(err) {...}` block we analyze the error object `err`. +2. If we don't know how to handle it, then we do `throw err`. + +In the code below, we use rethrowing so that `catch` only handles `SyntaxError`: + +```js run +let json = '{ "age": 30 }'; // incomplete data +try { + + let user = JSON.parse(json); + + if (!user.name) { + throw new SyntaxError("Incomplete data: no name"); + } + +*!* + blabla(); // unexpected error +*/!* + + alert( user.name ); + +} catch(e) { + +*!* + if (e.name == "SyntaxError") { + alert( "JSON Error: " + e.message ); + } else { + throw e; // rethrow (*) + } +*/!* + +} +``` + +The error throwing on line `(*)` from inside `catch` block "falls out" of `try..catch` and can be either caught by an outer `try..catch` construct (if it exists), or it kills the script. + +So the `catch` block actually handles only errors that it knows how to deal with and "skips" all others. + +The example below demonstrates how such errors can be caught by one more level of `try..catch`: + +```js run +function readData() { + let json = '{ "age": 30 }'; + + try { + // ... +*!* + blabla(); // error! +*/!* + } catch (e) { + // ... + if (e.name != 'SyntaxError') { +*!* + throw e; // rethrow (don't know how to deal with it) +*/!* + } + } +} + +try { + readData(); +} catch (e) { +*!* + alert( "External catch got: " + e ); // caught it! +*/!* +} +``` + +Here `readData` only knows how to handle `SyntaxError`, while the outer `try..catch` knows how to handle everything. + +## try..catch..finally + +Wait, that's not all. + +The `try..catch` construct may have one more code clause: `finally`. + +If it exists, it runs in all cases: + +- after `try`, if there were no errors, +- after `catch`, if there were errors. + +The extended syntax looks like this: + +```js +*!*try*/!* { + ... try to execute the code ... +} *!*catch*/!*(e) { + ... handle errors ... +} *!*finally*/!* { + ... execute always ... +} +``` + +Try running this code: + +```js run +try { + alert( 'try' ); + if (confirm('Make an error?')) BAD_CODE(); +} catch (e) { + alert( 'catch' ); +} finally { + alert( 'finally' ); +} +``` + +The code has two ways of execution: + +1. If you answer "Yes" to "Make an error?", then `try -> catch -> finally`. +2. If you say "No", then `try -> finally`. + +The `finally` clause is often used when we start doing something before `try..catch` and want to finalize it in any case of outcome. + +For instance, we want to measure time that a Fibonacci numbers function `fib(n)` takes. Naturally, we can start measuring before it runs and finish afterwards. But what if there's an error during the function call? In particular, the implementation of `fib(n)` in the code below returns an error for negative or non-integer numbers. + +The `finally` clause is a great place to finish the measurements no matter what. + +Here `finally` guarantees that the time will be measured correctly in both situations -- in case of a successful execution of `fib` and in case of an error in it: + +```js run +let num = +prompt("Enter a positive integer number?", 35) + +let diff, result; + +function fib(n) { + if (n < 0 || Math.trunc(n) != n) { + throw new Error("Must not be negative, and also an integer."); + } + return n <= 1 ? n : fib(n - 1) + fib(n - 2); +} + +let start = Date.now(); + +try { + result = fib(num); +} catch (e) { + result = 0; +*!* +} finally { + diff = Date.now() - start; +} +*/!* + +alert(result || "error occured"); + +alert( `execution took ${diff}ms` ); +``` + +You can check by running the code with entering `35` into `prompt` -- it executes normally, `finally` after `try`. And then enter `-1` -- there will be an immediate error, an the execution will take `0ms`. Both measurements are done correctly. + +In other words, there may be two ways to exit a function: either a `return` or `throw`. The `finally` clause handles them both. + + +```smart header="Variables are local inside `try..catch..finally`" +Please note that `result` and `diff` variables in the code above are declared *before* `try..catch`. + +Otherwise, if `let` were made inside the `{...}` block, it would only be visible inside of it. +``` + +````smart header="`finally` and `return`" +Finally clause works for *any* exit from `try..catch`. That includes an explicit `return`. + +In the example below, there's a `return` in `try`. In this case, `finally` is executed just before the control returns to the outer code. + +```js run +function func() { + + try { +*!* + return 1; +*/!* + + } catch (e) { + /* ... */ + } finally { +*!* + alert( 'finally' ); +*/!* + } +} + +alert( func() ); // first works alert from finally, and then this one +``` +```` + +````smart header="`try..finally`" + +The `try..finally` construct, without `catch` clause, is also useful. We apply it when we don't want to handle errors right here, but want to be sure that processes that we started are finalized. + +```js +function func() { + // start doing something that needs completion (like measurements) + try { + // ... + } finally { + // complete that thing even if all dies + } +} +``` +In the code above, an error inside `try` always falls out, because there's no `catch`. But `finally` works before the execution flow jumps outside. +```` + +## Global catch + +```warn header="Environment-specific" +The information from this section is not a part of the core JavaScript. +``` + +Let's imagine we've got a fatal error outside of `try..catch`, and the script died. Like a programming error or something else terrible. + +Is there a way to react on such occurrences? We may want to log the error, show something to the user (normally he doesn't see error messages) etc. + +There is none in the specification, but environments usually provide it, because it's really useful. For instance, Node.JS has [process.on('uncaughtException')](https://nodejs.org/api/process.html#process_event_uncaughtexception) for that. And in the browser we can assign a function to special [window.onerror](mdn:api/GlobalEventHandlers/onerror) property. It will run in case of an uncaught error. + +The syntax: + +```js +window.onerror = function(message, url, line, col, error) { + // ... +}; +``` + +`message` +: Error message. + +`url` +: URL of the script where error happened. + +`line`, `col` +: Line and column numbers where error happened. + +`error` +: Error object. + +For instance: + +```html run untrusted refresh height=1 + +``` + +The role of the global handler `window.onerror` is usually not to recover the script execution -- that's probably impossible in case of programming errors, but to send the error message to developers. + +There are also web-services that provide error-logging for such cases, like or . + +They work like this: + +1. We register at the service and get a piece of JS (or a script URL) from them to insert on pages. +2. That JS script has a custom `window.onerror` function. +3. When an error occurs, it sends a network request about it to the service. +4. We can log in to the service web interface and see errors. + +## Summary + +The `try..catch` construct allows to handle runtime errors. It literally allows to try running the code and catch errors that may occur in it. + +The syntax is: + +```js +try { + // run this code +} catch(err) { + // if an error happened, then jump here + // err is the error object +} finally { + // do in any case after try/catch +} +``` + +There may be no `catch` section or no `finally`, so `try..catch` and `try..finally` are also valid. + +Error objects have following properties: + +- `message` -- the human-readable error message. +- `name` -- the string with error name (error constructor name). +- `stack` (non-standard) -- the stack at the moment of error creation. + +We can also generate our own errors using the `throw` operator. Technically, the argument of `throw` can be anything, but usually it's an error object inheriting from the built-in `Error` class. More on extending errors in the next chapter. + +Rethrowing is a basic pattern of error handling: a `catch` block usually expects and knows how to handle the particular error type, so it should rethrow errors it doesn't know. + +Even if we don't have `try..catch`, most environments allow to setup a "global" error handler to catch errors that "fall out". In-browser that's `window.onerror`. diff --git a/1-js/08-error-handling/1-try-catch/try-catch-flow.png b/1-js/08-error-handling/1-try-catch/try-catch-flow.png new file mode 100644 index 00000000..6a91b632 Binary files /dev/null and b/1-js/08-error-handling/1-try-catch/try-catch-flow.png differ diff --git a/1-js/08-error-handling/1-try-catch/try-catch-flow@2x.png b/1-js/08-error-handling/1-try-catch/try-catch-flow@2x.png new file mode 100644 index 00000000..8bf9680f Binary files /dev/null and b/1-js/08-error-handling/1-try-catch/try-catch-flow@2x.png differ diff --git a/1-js/08-error-handling/2-custom-errors/1-format-error/solution.md b/1-js/08-error-handling/2-custom-errors/1-format-error/solution.md new file mode 100644 index 00000000..bb6b74cf --- /dev/null +++ b/1-js/08-error-handling/2-custom-errors/1-format-error/solution.md @@ -0,0 +1,16 @@ +```js run untrusted +class FormatError extends SyntaxError { + constructor(message) { + super(message); + this.name = "FormatError"; + } +} + +let err = new FormatError("formatting error"); + +alert( err.message ); // formatting error +alert( err.name ); // FormatError +alert( err.stack ); // stack + +alert( err instanceof SyntaxError ); // true +``` diff --git a/1-js/08-error-handling/2-custom-errors/1-format-error/task.md b/1-js/08-error-handling/2-custom-errors/1-format-error/task.md new file mode 100644 index 00000000..2c8e910f --- /dev/null +++ b/1-js/08-error-handling/2-custom-errors/1-format-error/task.md @@ -0,0 +1,22 @@ +importance: 5 + +--- + +# Inherit from SyntaxError + +Create a class `FormatError` that inherits from the built-in `SyntaxError` class. + +It should support `message`, `name` and `stack` properties. + +Usage example: + +```js +let err = new FormatError("formatting error"); + +alert( err.message ); // formatting error +alert( err.name ); // FormatError +alert( err.stack ); // stack + +alert( err instanceof FormatError ); // true +alert( err instanceof SyntaxError ); // true (because inherits from SyntaxError) +``` diff --git a/1-js/08-error-handling/2-custom-errors/article.md b/1-js/08-error-handling/2-custom-errors/article.md new file mode 100644 index 00000000..d7a780f6 --- /dev/null +++ b/1-js/08-error-handling/2-custom-errors/article.md @@ -0,0 +1,307 @@ +# Custom errors, extending Error + +When we develop something, we often need our own error classes to reflect specific things that may go wrong in our tasks. For errors in network operations we may need `HttpError`, for database operations `DbError`, for searching operations `NotFoundError` and so on. + +Our errors should support basic error properties like `message`, `name` and, preferably, `stack`. But they also may have other properties of their own, e.g. `HttpError` objects may have `statusCode` property with a value like `404` or `403` or `500`. + +JavaScript allows to use `throw` with any argument, so technically our custom error classes don't need to inherit from `Error`. But if we inherit, then it becomes possible to use `obj instanceof Error` to identify error objects. So it's better to inherit from it. + +As we build our application, our own errors naturally form a hierarchy, for instance `HttpTimeoutError` may inherit from `HttpError`, and so on. + +## Extending Error + +As an example, let's consider a function `readUser(json)` that should read JSON with user data. + +Here's an example of how a valid `json` may look: +```js +let json = `{ "name": "John", "age": 30 }`; +``` + +Internally, we'll use `JSON.parse`. If it receives malformed `json`, then it throws `SyntaxError`. + +But even if `json` is syntactically correct, that doesn't mean that it's a valid user, right? It may miss the necessary data. For instance, if may not have `name` and `age` properties that are essential for our users. + +Our function `readUser(json)` will not only read JSON, but check ("validate") the data. If there are no required fields, or the format is wrong, then that's an error. And that's not a `SyntaxError`, because the data is syntactically correct, but another kind of error. We'll call it `ValidationError` and create a class for it. An error of that kind should also carry the information about the offending field. + +Our `ValidationError` class should inherit from the built-in `Error` class. + +That class is built-in, but we should have its approximate code before our eyes, to understand what we're extending. + +So here you are: + +```js +// The "pseudocode" for the built-in Error class defined by JavaScript itself +class Error { + constructor(message) { + this.message = message; + this.name = "Error"; // (different names for different built-in error classes) + this.stack = ; // non-standard, but most environments support it + } +} +``` + +Now let's go on and inherit `ValidationError` from it: + +```js run untrusted +*!* +class ValidationError extends Error { +*/!* + constructor(message) { + super(message); // (1) + this.name = "ValidationError"; // (2) + } +} + +function test() { + throw new ValidationError("Whoops!"); +} + +try { + test(); +} catch(err) { + alert(err.message); // Whoops! + alert(err.name); // ValidationError + alert(err.stack); // a list of nested calls with line numbers for each +} +``` + +Please take a look at the constructor: + +1. In the line `(1)` we call the parent constructor. JavaScript requires us to call `super` in the child constructor, so that's obligatory. The parent constructor sets the `message` property. +2. The parent constructor also sets the `name` property to `"Error"`, so in the line `(2)` we reset it to the right value. + +Let's try to use it in `readUser(json)`: + +```js run +class ValidationError extends Error { + constructor(message) { + super(message); + this.name = "ValidationError"; + } +} + +// Usage +function readUser(json) { + let user = JSON.parse(json); + + if (!user.age) { + throw new ValidationError("No field: age"); + } + if (!user.name) { + throw new ValidationError("No field: name"); + } + + return user; +} + +// Working example with try..catch + +try { + let user = readUser('{ "age": 25 }'); +} catch (err) { + if (err instanceof ValidationError) { +*!* + alert("Invalid data: " + err.message); // Invalid data: No field: name +*/!* + } else if (err instanceof SyntaxError) { // (*) + alert("JSON Syntax Error: " + err.message); + } else { + throw err; // unknown error, rethrow it (**) + } +} +``` + +The `try..catch` block in the code above handles both our `ValidationError` and the built-in `SyntaxError` from `JSON.parse`. + +Please take a look at how we use `instanceof` to check for the specific error type in the line `(*)`. + +We could also look at `err.name`, like this: + +```js +// ... +// instead of (err instanceof SyntaxError) +} else if (err.name == "SyntaxError") { // (*) +// ... +``` + +The `instanceof` version is much better, because in the future we are going to extend `ValidationError`, make subtypes of it, like `PropertyRequiredError`. And `instanceof` check will continue to work for new inheriting classes. So that's future-proof. + +Also it's important that if `catch` meets an unknown error, then it rethrows it in the line `(**)`. The `catch` only knows how to handle validation and syntax errors, other kinds (due to a typo in the code or such) should fall through. + +## Further inheritance + +The `ValidationError` class is very generic. Many things may go wrong. The property may be absent or it may be in a wrong format (like a string value for `age`). Let's make a more concrete class `PropertyRequiredError`, exactly for absent properties. It will carry additional information about the property that's missing. + +```js run +class ValidationError extends Error { + constructor(message) { + super(message); + this.name = "ValidationError"; + } +} + +*!* +class PropertyRequiredError extends ValidationError { + constructor(property) { + super("No property: " + property); + this.name = "PropertyRequiredError"; + this.property = property; + } +} +*/!* + +// Usage +function readUser(json) { + let user = JSON.parse(json); + + if (!user.age) { + throw new PropertyRequiredError("age"); + } + if (!user.name) { + throw new PropertyRequiredError("name"); + } + + return user; +} + +// Working example with try..catch + +try { + let user = readUser('{ "age": 25 }'); +} catch (err) { + if (err instanceof ValidationError) { +*!* + alert("Invalid data: " + err.message); // Invalid data: No property: name + alert(err.name); // PropertyRequiredError + alert(err.property); // name +*/!* + } else if (err instanceof SyntaxError) { + alert("JSON Syntax Error: " + err.message); + } else { + throw err; // unknown error, rethrow it + } +} +``` + +The new class `PropertyRequiredError` is easy to use: we only need to pass the property name: `new PropertyRequiredError(property)`. The human-readable `message` is generated by the constructor. + +Please note that `this.name` in `PropertyRequiredError` constructor is again assigned manually. That may become a bit tedius -- to assign `this.name = ` when creating each custom error. But there's a way out. We can make our own "basic error" class that removes this burden from our shoulders by using `this.constructor.name` for `this.name` in the constructor. And then inherit from it. + +Let's call it `MyError`. + +Here's the code with `MyError` and other custom error classes, simplified: + +```js run +class MyError extends Error { + constructor(message) { + super(message); +*!* + this.name = this.constructor.name; +*/!* + } +} + +class ValidationError extends MyError { } + +class PropertyRequiredError extends ValidationError { + constructor(property) { + super("No property: " + property); + this.property = property; + } +} + +// name is correct +alert( new PropertyRequiredError("field").name ); // PropertyRequiredError +``` + +Now custom errors are much shorter, especially `ValidationError`, as we got rid of the `"this.name = ..."` line in the constructor. + +## Wrapping exceptions + +The purpose of the function `readUser` in the code above is "to read the user data", right? There may occur different kinds of errors in the process. Right now we have `SyntaxError` and `ValidationError`, but in the future `readUser` function may grow: the new code will probably generate other kinds of errors. + +The code which calls `readUser` should handle these errors. Right now it uses multiple `if` in the `catch` block to check for different error types and rethrow the unknown ones. But if `readUser` function generates several kinds of errors -- then we should ask ourselves: do we really want to check for all error types one-by-one in every code that calls `readUser`? + +Often the answer is "No": the outer code wants to be "one level above all that". It wants to have some kind of "data reading error". Why exactly it happened -- is often irrelevant (the error message describes it). Or, even better if there is a way to get error details, but only if we need to. + +So let's make a new class `ReadError` to represent such errors. If an error occurs inside `readUser`, we'll catch it there and generate `ReadError`. We'll also keep the reference to the original error in the `cause` property. Then the outer code will only have to check for `ReadError`. + +Here's the code that defines `ReadError` and demonstrates its use in `readUser` and `try..catch`: + +```js run +class ReadError extends Error { + constructor(message, cause) { + super(message); + this.cause = cause; + this.name = 'ReadError'; + } +} + +class ValidationError extends Error { /*...*/ } +class PropertyRequiredError extends ValidationError { /* ... */ } + +function validateUser(user) { + if (!user.age) { + throw new PropertyRequiredError("age"); + } + + if (!user.name) { + throw new PropertyRequiredError("name"); + } +} + +function readUser(json) { + let user; + + try { + user = JSON.parse(json); + } catch (err) { +*!* + if (err instanceof SyntaxError) { + throw new ReadError("Syntax Error", err); + } else { + throw err; + } +*/!* + } + + try { + validateUser(user); + } catch (err) { +*!* + if (err instanceof ValidationError) { + throw new ReadError("Validation Error", err); + } else { + throw err; + } +*/!* + } + +} + +try { + readUser('{bad json}'); +} catch (e) { + if (e instanceof ReadError) { +*!* + alert(e); + // Original error: SyntaxError: Unexpected token b in JSON at position 1 + alert("Original error: " + e.cause); +*/!* + } else { + throw e; + } +} +``` + +In the code above, `readUser` works exactly as described -- catches syntax and validation errors and throws `ReadError` errors instead (unknown errors are rethrown as usual). + +So the outer code checks `instanceof ReadError` and that's it. No need to list possible all error types. + +The approach is called "wrapping exceptions", because we take "low level exceptions" and "wrap" them into `ReadError` that is more abstract and more convenient to use for the calling code. It is widely used in object-oriented programming. + +## Summary + +- We can inherit from `Error` and other built-in error classes normally, just need to take care of `name` property and don't forget to call `super`. +- Most of the time, we should use `instanceof` to check for particular errors. It also works with inheritance. But sometimes we have an error object coming from the 3rd-party library and there's no easy way to get the class. Then `name` property can be used for such checks. +- Wrapping exceptions is a widespread technique when a function handles low-level exceptions and makes a higher-level object to report about the errors. Low-level exceptions sometimes become properties of that object like `err.cause` in the examples above, but that's not strictly required. diff --git a/1-js/08-error-handling/index.md b/1-js/08-error-handling/index.md new file mode 100644 index 00000000..face61c6 --- /dev/null +++ b/1-js/08-error-handling/index.md @@ -0,0 +1 @@ +# Error handling diff --git a/1-js/1-getting-started/1-intro/article.md b/1-js/1-getting-started/1-intro/article.md deleted file mode 100644 index 75db80f1..00000000 --- a/1-js/1-getting-started/1-intro/article.md +++ /dev/null @@ -1,234 +0,0 @@ -# Введение в JavaScript - -Давайте посмотрим, что такого особенного в JavaScript, почему именно он, и какие еще технологии существуют, кроме JavaScript. - -## Что такое JavaScript? - -*JavaScript* изначально создавался для того, чтобы сделать web-странички "живыми". -Программы на этом языке называются *скриптами*. В браузере они подключаются напрямую к HTML и, как только загружается страничка -- тут же выполняются. - -**Программы на JavaScript -- обычный текст**. Они не требуют какой-то специальной подготовки. - -В этом плане JavaScript сильно отличается от другого языка, который называется [Java](http://ru.wikipedia.org/wiki/Java). - -[smart header="Почему JavaScript?"] -Когда создавался язык JavaScript, у него изначально было другое название: "LiveScript". Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным. - -Планировалось, что JavaScript будет эдаким "младшим братом" Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется [ECMAScript](https://ru.wikipedia.org/wiki/ECMAScript), и к Java не имеет никакого отношения. - -У него много особенностей, которые усложняют освоение, но по ходу учебника мы с ними разберёмся. -[/smart] - -JavaScript может выполняться не только в браузере, а где угодно, нужна лишь специальная программа -- [интерпретатор](http://ru.wikipedia.org/wiki/%D0%98%D0%BD%D1%82%D0%B5%D1%80%D0%BF%D1%80%D0%B5%D1%82%D0%B0%D1%82%D0%BE%D1%80). Процесс выполнения скрипта называют "интерпретацией". - -[smart header="Компиляция и интерпретация, для программистов"] -Для выполнения программ, не важно на каком языке, существуют два способа: "компиляция" и "интерпретация". - -
    -
  • *Компиляция* -- это когда исходный код программы, при помощи специального инструмента, другой программы, которая называется "компилятор", преобразуется в другой язык, как правило -- в машинный код. Этот машинный код затем распространяется и запускается. При этом исходный код программы остаётся у разработчика.
  • -
  • *Интерпретация* -- это когда исходный код программы получает другой инструмент, который называют "интерпретатор", и выполняет его "как есть". При этом распространяется именно сам исходный код (скрипт). Этот подход применяется в браузерах для JavaScript.
  • -
- -Современные интерпретаторы перед выполнением преобразуют JavaScript в машинный код или близко к нему, оптимизируют, а уже затем выполняют. И даже во время выполнения стараются оптимизировать. Поэтому JavaScript работает очень быстро. -[/smart] - -Во все основные браузеры встроен интерпретатор JavaScript, именно поэтому они могут выполнять скрипты на странице. Но, разумеется, JavaScript можно использовать не только в браузере. Это полноценный язык, программы на котором можно запускать и на сервере, и даже в стиральной машинке, если в ней установлен соответствующий интерпретатор. - -[warn header="Поговорим о браузерах"] - -Далее в этой главе мы говорим о возможностях и ограничениях JavaScript именно в контексте браузера. - -[/warn] - -## Что умеет JavaScript? - -Современный JavaScript -- это "безопасный" язык программирования общего назначения. Он не предоставляет низкоуровневых средств работы с памятью, процессором, так как изначально был ориентирован на браузеры, в которых это не требуется. - -Что же касается остальных возможностей -- они зависят от окружения, в котором запущен JavaScript. В браузере JavaScript умеет делать всё, что относится к манипуляции со страницей, взаимодействию с посетителем и, в какой-то мере, с сервером: - -
    -
  • Создавать новые HTML-теги, удалять существующие, менять стили элементов, прятать, показывать элементы и т.п.
  • -
  • Реагировать на действия посетителя, обрабатывать клики мыши, перемещения курсора, нажатия на клавиатуру и т.п.
  • -
  • Посылать запросы на сервер и загружать данные без перезагрузки страницы (эта технология называется "AJAX").
  • -
  • Получать и устанавливать cookie, запрашивать данные, выводить сообщения...
  • -
  • ...и многое, многое другое!
  • -
- -## Что НЕ умеет JavaScript? - -JavaScript -- быстрый и мощный язык, но браузер накладывает на его исполнение некоторые ограничения.. - -Это сделано для безопасности пользователей, чтобы злоумышленник не мог с помощью JavaScript получить личные данные или как-то навредить компьютеру пользователя. - -Этих ограничений нет там, где JavaScript используется вне браузера, например на сервере. Кроме того, современные браузеры предоставляют свои механизмы по установке плагинов и расширений, которые обладают расширенными возможностями, но требуют специальных действий по установке от пользователя - -**Большинство возможностей JavaScript в браузере ограничено текущим окном и страницей.** - - - -
    -
  • JavaScript не может читать/записывать произвольные файлы на жесткий диск, копировать их или вызывать программы. Он не имеет прямого доступа к операционной системе. - -Современные браузеры могут работать с файлами, но эта возможность ограничена специально выделенной директорией -- *"песочницей"*. Возможности по доступу к устройствам также прорабатываются в современных стандартах и частично доступны в некоторых браузерах. -
  • -
  • JavaScript, работающий в одной вкладке, не может общаться с другими вкладками и окнами, за исключением случая, когда он сам открыл это окно или несколько вкладок из одного источника (одинаковый домен, порт, протокол). - -Есть способы это обойти, и они раскрыты в учебнике, но они требуют специального кода на оба документа, которые находятся в разных вкладках или окнах. Без него, из соображений безопасности, залезть из одной вкладки в другую при помощи JavaScript нельзя. -
  • -
  • Из JavaScript можно легко посылать запросы на сервер, с которого пришла страница. Запрос на другой домен тоже возможен, но менее удобен, т. к. и здесь есть ограничения безопасности. -
  • -
- -## В чём уникальность JavaScript? - -Есть как минимум *три* замечательных особенности JavaScript: - -[compare] -+Полная интеграция с HTML/CSS. -+Простые вещи делаются просто. -+Поддерживается всеми распространёнными браузерами и включён по умолчанию. -[/compare] - -**Этих трёх вещей одновременно нет больше ни в одной браузерной технологии.** - -Поэтому JavaScript и является самым распространённым средством создания браузерных интерфейсов. - -## Тенденции развития - -Перед тем, как вы планируете изучить новую технологию, полезно ознакомиться с её развитием и перспективами. Здесь в JavaScript всё более чем хорошо. - -### HTML 5 - -*HTML 5* -- эволюция стандарта HTML, добавляющая новые теги и, что более важно, ряд новых возможностей браузерам. - -Вот несколько примеров: -
    -
  • Чтение/запись файлов на диск (в специальной "песочнице", то есть не любые).
  • -
  • Встроенная в браузер база данных, которая позволяет хранить данные на компьютере пользователя.
  • -
  • Многозадачность с одновременным использованием нескольких ядер процессора.
  • -
  • Проигрывание видео/аудио, без Flash.
  • -
  • 2D и 3D-рисование с аппаратной поддержкой, как в современных играх.
  • -
- -Многие возможности HTML5 всё ещё в разработке, но браузеры постепенно начинают их поддерживать. - -[summary]Тенденция: JavaScript становится всё более и более мощным и возможности браузера растут в сторону десктопных приложений.[/summary] - -### EcmaScript 6 - -Сам язык JavaScript улучшается. Современный стандарт EcmaScript 5 включает в себя новые возможности для разработки, EcmaScript 6 будет шагом вперёд в улучшении синтаксиса языка. - -Современные браузеры улучшают свои движки, чтобы увеличить скорость исполнения JavaScript, исправляют баги и стараются следовать стандартам. - -[summary]Тенденция: JavaScript становится всё быстрее и стабильнее, в язык добавляются новые возможности.[/summary] - -Очень важно то, что новые стандарты HTML5 и ECMAScript сохраняют максимальную совместимость с предыдущими версиями. Это позволяет избежать неприятностей с уже существующими приложениями. - -Впрочем, небольшая проблема с "супер-современными штучками" всё же есть. Иногда браузеры стараются включить новые возможности, которые ещё не полностью описаны в стандарте, но настолько интересны, что разработчики просто не могут ждать. - -...Однако, со временем стандарт меняется и браузерам приходится подстраиваться к нему, что может привести к ошибкам в уже написанном, основанном на старой реализации, JavaScript-коде. Поэтому следует дважды подумать перед тем, как применять на практике такие "супер-новые" решения. - -При этом все браузеры сходятся к стандарту, и различий между ними уже гораздо меньше, чем всего лишь несколько лет назад. - -[summary]Тенденция: всё идет к полной совместимости со стандартом.[/summary] - - -## Альтернативные браузерные технологии - -Вместе с JavaScript на страницах используются и другие технологии. Связка с ними может помочь достигнуть более интересных результатов в тех местах, где браузерный JavaScript пока не столь хорош, как хотелось бы. - -### Java - -Java -- язык общего назначения, на нём можно писать самые разные программы. Для интернет-страниц есть особая возможность - написание *апплетов*. - -*Апплет* -- это программа на языке Java, которую можно подключить к HTML при помощи тега `applet`, выглядит это примерно так: - -```html - - - - - -``` - -Такой тег загружает Java-программу из файла `BTApplet.class` и выполняет её с параметрами `param`. Апплет выполняется в отдельной части страницы, в прямоугольном "контейнере". Все действия пользователя внутри него обрабатывает апплет. Контейнер, впрочем, может быть и спрятан, если апплету нечего показывать. - -Конечно, для этого на компьютере должна быть установлена и включена среда выполнения Java, включая браузерный плагин. Кроме того, апплет должен быть подписан сертификатом издателя (в примере выше апплет без подписи), иначе Java заблокирует его. - -**Чем нам, JavaScript-разработчикам, может быть интересен Java?** - -В первую очередь тем, что подписанный Java-апплет может всё то же, что и обычная программа, установленная на компьютере посетителя. Конечно, для этого понадобится согласие пользователя при открытии такого апплета. - -[compare] -+Java может делать *всё* от имени посетителя, совсем как установленная программа. Потенциально опасные действия требуют подписанного апплета и согласия пользователя. --Java требует больше времени для загрузки. --Среда выполнения Java, включая браузерный плагин, должна быть установлена на компьютере посетителя и включена. --Java-апплет не интегрирован с HTML-страницей, а выполняется отдельно. Но он может вызывать функции JavaScript. -[/compare] - - -### Плагины и расширения для браузера - -Все современные браузеры предоставляют возможность написать плагины. Для этого можно использовать как JavaScript (Chrome, Opera, Firefox), так и язык С (ActiveX для Internet Explorer). - -Эти плагины могут как отображать содержимое специального формата (плагин для проигрывания музыки, для показа PDF), так и взаимодействовать со страницей. - -Как и в ситуации с Java-апплетом, у них широкие возможности, но посетитель поставит их в том случае, если вам доверяет. - -### Adobe Flash - -Adobe Flash -- кросс-браузерная платформа для мультимедиа-приложений, анимаций, аудио и видео. - -*Flash-ролик* -- это скомпилированная программа, написанная на языке ActionScript. Её можно подключить к HTML-странице и запустить в прямоугольном контейнере. - -В первую очередь Flash полезен тем, что позволяет **кросс-браузерно** работать с микрофоном, камерой, с буфером обмена, а также поддерживает продвинутые возможности по работе с сетевыми соединениями. - -[compare] -+Сокеты, UDP для P2P и другие продвинутые возможности по работе с сетевыми соединениями -+Поддержка мультимедиа: изображения, аудио, видео. Работа с веб-камерой и микрофоном. --Flash должен быть установлен и включён. А на некоторых устройствах он вообще не поддерживается. --Flash не интегрирован с HTML-страницей, а выполняется отдельно. --Существуют ограничения безопасности, однако они немного другие, чем в JavaScript. -[/compare] - -Из Flash можно вызывать JavaScript и наоборот, поэтому обычно сайты используют JavaScript, а там, где он не справляется -- можно подумать о Flash. - - -## Языки поверх JavaScript - - -Синтаксис JavaScript устраивает не всех: одним он кажется слишком свободным, другим -- наоборот, слишком ограниченным, третьи хотят добавить в язык дополнительные возможности, которых нет в стандарте... - -Это нормально, ведь требования и проекты у всех разные. - -В последние годы появилось много языков, которые добавляют различные возможности "поверх" JavaScript, а для запуска в браузере -- при помощи специальных инструментов "трансляторов" превращаются в обычный JavaScript-код. - -Это преобразование происходит автоматически и совершенно прозрачно, при этом неудобств в разработке и отладке практически нет. - -При этом разные языки выглядят по-разному и добавляют совершенно разные вещи: - -
    -
  • Язык [CoffeeScript](http://coffeescript.org/) -- это "синтаксический сахар" поверх JavaScript. Он сосредоточен на большей ясности и краткости кода. Как правило, его особенно любят программисты на Ruby.
  • -
  • Язык [TypeScript](http://www.typescriptlang.org/) сосредоточен на добавлении строгой типизации данных. Он предназначен для упрощения разработки и поддержки больших систем. Его разрабатывает Microsoft.
  • -
  • Язык [Dart](https://www.dartlang.org/) интересен тем, что он не только транслируется в JavaScript, как и другие языки, но и имеет свою независимую среду выполнения, которая даёт ему ряд возможностей и доступна для встраивания в приложения (вне браузера). Он разрабатывается компанией Google.
  • -
- -[smart header="ES6 и ES7 прямо сейчас"] -Существуют также трансляторы, которые берут код, использующий возможности будущих стандартов JavaScript, и преобразуют его в более старый вариант, который понимают все браузеры. - -Например, [babeljs](https://babeljs.io/). - -Благодаря этому, мы можем использовать многие возможности будущего уже сегодня. -[/smart] - - -## Итого - -Язык JavaScript уникален благодаря своей полной интеграции с HTML/CSS. Он работает почти у всех посетителей. - -...Но хороший JavaScript-программист не должен забывать и о других технологиях. - -Ведь наша цель -- создание хороших приложений, и здесь Flash, Java и браузерные расширения имеют свои уникальные возможности, которые можно использовать вместе с JavaScript. - -Что же касается CoffeeScript, TypeScript и других языков, построенных над JavaScript -- они могут быть очень полезны. Рекомендуется посмотреть их, хотя бы в общих чертах, но, конечно, уже после освоения самого JavaScript. - diff --git a/1-js/1-getting-started/1-intro/limitations.png b/1-js/1-getting-started/1-intro/limitations.png deleted file mode 100644 index 0f58b9d6..00000000 Binary files a/1-js/1-getting-started/1-intro/limitations.png and /dev/null differ diff --git a/1-js/1-getting-started/1-intro/limitations@2x.png b/1-js/1-getting-started/1-intro/limitations@2x.png deleted file mode 100644 index e790016b..00000000 Binary files a/1-js/1-getting-started/1-intro/limitations@2x.png and /dev/null differ diff --git a/1-js/1-getting-started/2-pre-coding/article.md b/1-js/1-getting-started/2-pre-coding/article.md deleted file mode 100644 index 05833be2..00000000 --- a/1-js/1-getting-started/2-pre-coding/article.md +++ /dev/null @@ -1,118 +0,0 @@ -# Справочники и спецификации - -В этом разделе мы познакомимся со справочниками и спецификациями. - -Если вы только начинаете изучение, то вряд ли они будут нужны прямо сейчас. Тем не менее, эта глава находится в начале, так как предсказать точный момент, когда вы захотите заглянуть в справочник -- невозможно, но точно известно, что этот момент настанет. - -Поэтому рекомендуется кратко взглянуть на них и взять на заметку, чтобы при необходимости вернуться к ним в будущем. - -[cut] - -## Справочники, и как в них искать - -Самая полная и подробная информация по JavaScript и браузерам есть в справочниках. - -Её объём таков, что перевести всё с английского невозможно. Даже сделать "единый полный справочник" не получается, так как изменений много и они происходят постоянно. - -Тем не менее, жить вполне можно если знать, куда смотреть. - -**Есть три основных справочника по JavaScript на английском языке**: - -
    -
  1. [Mozilla Developer Network](https://developer.mozilla.org/) -- содержит информацию, верную для основных браузеров. Также там присутствуют расширения только для Firefox (они помечены). - -Когда мне нужно быстро найти "стандартную" информацию по `RegExp` - ввожу в Google **"RegExp MDN"**, и ключевое слово "MDN" (Mozilla Developer Network) приводит к информации из этого справочника. -
  2. -
  3. [MSDN](http://msdn.microsoft.com) -- справочник от Microsoft. Там много информации, в том числе и по JavaScript (они называют его "JScript"). Если нужно что-то, специфичное для IE -- лучше лезть сразу туда. - -Например, для информации об особенностях `RegExp` в IE -- полезное сочетание: **"RegExp msdn"**. Иногда к поисковой фразе лучше добавить термин "JScript": **"RegExp msdn jscript"**.
  4. -
  5. [Safari Developer Library](https://developer.apple.com/library/safari/navigation/index.html) -- менее известен и используется реже, но в нём тоже можно найти ценную информацию.
  6. -
- -Есть ещё справочники, не от разработчиков браузеров, но тоже хорошие: - -
    -
  1. [http://help.dottoro.com]() -- содержит подробную информацию по HTML/CSS/JavaScript.
  2. -
  3. [http://javascript.ru/manual]() -- справочник по JavaScript на русском языке, он содержит основную информацию по языку, без функций для работы с документом. К нему можно обращаться и по адресу, если знаете, что искать. Например, так: [http://javascript.ru/RegExp](). -
  4. -
  5. [http://www.quirksmode.org]() -- информация о браузерных несовместимостях. Этот ресурс сам по себе довольно старый и, в первую очередь, полезен для поддержки устаревших браузеров. Для поиска можно пользоваться комбинацией **"quirksmode onkeypress"** в Google.
  6. -
  7. [http://caniuse.com]() -- ресурс о поддержке браузерами новейших возможностей HTML/CSS/JavaScript. Например, для поддержки функций криптографии: [http://caniuse.com/#feat=cryptography](). -
  8. -
- -## Спецификации - -Спецификация -- это самый главный, определяющий документ, в котором написано, как себя ведёт JavaScript, браузер, CSS и т.п. - -Если что-то непонятно, и справочник не даёт ответ, то спецификация, как правило, раскрывает тему гораздо глубже и позволяет расставить точки над i. - -### Спецификация ECMAScript - -Спецификация (формальное описание синтаксиса, базовых объектов и алгоритмов) языка Javascript называется [ECMAScript](http://www.ecma-international.org/publications/standards/Ecma-262.htm). - -Её перевод есть на сайте в разделе [стандарт языка](http://es5.javascript.ru/). - -[smart header="Почему не просто "JavaScript" ?"] -Вы можете спросить: "Почему спецификация для JavaScript не называется просто *"JavaScript"*, зачем существует какое-то отдельное название?" - -Всё потому, что JavaScript™ -- зарегистрированная торговая марка, принадлежащая корпорации Oracle. - -Название "ECMAScript" было выбрано, чтобы сохранить спецификацию независимой от владельцев торговой марки. -[/smart] - -Спецификация может рассказать многое о том, как работает язык, и она является самым фундаментальным, доверенным источником информации. - -### Спецификации HTML/DOM/CSS - -JavaScript -- язык общего назначения, поэтому в спецификации ECMAScript нет ни слова о браузерах. - -Главная организация, которая занимается HTML, CSS, XML и множеством других стандартов -- [Консорциум Всемирной паутины](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BD%D1%81%D0%BE%D1%80%D1%86%D0%B8%D1%83%D0%BC_%D0%92%D1%81%D0%B5%D0%BC%D0%B8%D1%80%D0%BD%D0%BE%D0%B9_%D0%BF%D0%B0%D1%83%D1%82%D0%B8%D0%BD%D1%8B) (World Wide Consortium, сокращённо W3C). - -Информацию о них можно найти на сайте [w3.org](http://w3.org). К сожалению, найти в этой куче то, что нужно, может быть нелегко, особенно когда неизвестно в каком именно стандарте искать. Самый лучший способ -- попросить Google с указанием сайта. - -Например, для поиска `document.cookie` набрать [document.cookie site:w3.org](https://www.google.com/search?q=document.cookie+site%3Aw3.org). - -Последние версии стандартов расположены на домене [dev.w3.org](http://dev.w3.org). - -Кроме того, в том, что касается HTML5 и DOM/CSS, W3C активно использует наработки другой организации -- [WhatWG](https://whatwg.org/). Поэтому самые актуальные версии спецификаций по этим темам обычно находятся на [https://whatwg.org/specs/](). - -Иногда бывает так, что информация на сайте [http://dev.w3.org]() отличается от [http://whatwg.org](). В этом случае, как правило, следует руководствоваться [http://whatwg.org](). - -## Итого - -Итак, посмотрим какие у нас есть источники информации. - -Справочники: -
    -
  • Mozilla Developer Network -- информация для Firefox и большинства браузеров. -Google-комбо: `"RegExp MDN"`, ключевое слово "MDN".
  • -
  • MSDN -- информация по IE. -Google-комбо: `"RegExp msdn"`. Иногда лучше добавить термин "JScript": `"RegExp msdn jscript"`.
  • -
  • [Safari Developer Library](https://developer.apple.com/library/safari/navigation/index.html) -- информация по Safari.
  • -
  • http://help.dottoro.com -- подробная информация по HTML/CSS/JavaScript с учётом браузерной совместимости. -Google-комбо: `"RegExp dottoro"`.
  • -
  • [](http://javascript.ru/manual) -- справочник по JavaScript на русском языке. К нему можно обращаться и по адресу, если знаете, что искать. Например, так: [](http://javascript.ru/RegExp). -Google-комбо: `"RegExp site:javascript.ru"`. -
  • -
- -Спецификации содержат важнейшую информацию о том, как оно "должно работать": - -
    -
  • JavaScript, современный стандарт [ES5 (англ)](http://www.ecma-international.org/publications/standards/Ecma-262.htm), и предыдущий [ES3 (рус)](http://javascript.ru/ecma).
  • -
  • HTML/DOM/CSS -- на сайте [http://w3.org](http://www.w3.org). -Google-комбо: `"document.cookie site:w3.org"`.
  • -
  • ...А самые последние версии стандартов -- на [http://dev.w3.org]() и на [http://whatwg.org/specs/](https://whatwg.org/specs/).
  • -
- -То, как оно на самом деле работает и несовместимости: - -
    -
  • [http://quirksmode.org/](). Google-комбо: `"innerHeight quirksmode"`.
  • -
- -Поддержка современных и новейших возможностей браузерами: - -
    -
  • [http://caniuse.com](). Google-комбо: `"caniuse geolocation"`.
  • -
diff --git a/1-js/1-getting-started/3-editor/article.md b/1-js/1-getting-started/3-editor/article.md deleted file mode 100644 index 34cc4e85..00000000 --- a/1-js/1-getting-started/3-editor/article.md +++ /dev/null @@ -1,69 +0,0 @@ -# Редакторы для кода - -Для разработки обязательно нужен хороший редактор. - -Выбранный вами редактор должен иметь в своем арсенале: - -
    -
  1. Подсветку синтаксиса.
  2. -
  3. Автодополнение.
  4. -
  5. "Фолдинг" (от англ. folding) -- возможность скрыть-раскрыть блок кода.
  6. -
- -[cut] -## IDE - -Термин IDE (Integrated Development Environment) -- "интегрированная среда разработки", означает редактор, который расширен большим количеством "наворотов", умеет работать со вспомогательными системами, такими как багтрекер, контроль версий, и много чего ещё. - -Как правило, IDE загружает весь проект целиком, поэтому может предоставлять автодополнение по функциям всего проекта, удобную навигацию по его файлам и т.п. - -Если вы ещё не задумывались над выбором IDE, присмотритесь к следующим вариантам. - -
    -
  • Продукты IntelliJ: [WebStorm](http://www.jetbrains.com/webstorm/), а также в зависимости от дополнительного языка программирования [PHPStorm (PHP)](http://www.jetbrains.com/phpstorm/), [IDEA (Java)](http://www.jetbrains.com/idea/), [RubyMine (Ruby)](http://www.jetbrains.com/ruby/) и другие.
  • -
  • Visual Studio, в сочетании с разработкой под .NET (Win)
  • -
  • Продукты на основе Eclipse, в частности [Aptana](http://www.aptana.com/) и Zend Studio
  • -
  • [Komodo IDE](http://www.activestate.com/komodo-ide) и его облегчённая версия [Komodo Edit](http://www.activestate.com/komodo-edit).
  • -
  • [Netbeans](http://netbeans.org/)
  • -
- -Почти все они, за исключением Visual Studio, кросс-платформенные. - -Сортировка в этом списке ничего не означает. Выбор осуществляется по вкусу и по другим технологиям, которые нужно использовать вместе с JavaScript. - -Большинство IDE -- платные, с возможностью скачать и бесплатно использовать некоторое время. Но их стоимость, по сравнению с зарплатой веб-разработчика, невелика, поэтому ориентироваться можно на удобство. - -## Лёгкие редакторы - -Лёгкие редакторы -- не такие мощные, как IDE, но они быстрые и простые, мгновенно стартуют. - -Основная сфера применения лёгкого редактора -- мгновенно открыть нужный файл, чтобы что-то в нём поправить. - -На практике "лёгкие" редакторы могут обладать большим количеством плагинов, так что граница между IDE и "лёгким" редактором размыта, спорить что именно редактор, а что IDE -- не имеет смысла. - -Достойны внимания: - -
    -
  • Sublime Text (кросс-платформенный, shareware).
  • -
  • SciTe простой, легкий и очень быстрый (Windows, бесплатный).
  • -
  • Notepad++ (Windows, бесплатный).
  • -
  • Vim, Emacs. Если умеете их готовить.
  • -
- -## Мои редакторы - -Лично мои любимые редакторы: - -
    -
  • Как IDE -- редакторы от Jetbrains: для чистого JavaScript [WebStorm](http://www.jetbrains.com/webstorm/), если ещё какой-то язык, то в зависимости от языка: [PHPStorm (PHP)](http://www.jetbrains.com/phpstorm/), [IDEA (Java)](http://www.jetbrains.com/idea/), [RubyMine (Ruby)](http://www.jetbrains.com/ruby/). У них есть и другие редакторы под разные языки, но я ими не пользовался.
  • -
  • Как быстрый редактор -- Sublime Text.
  • -
  • Иногда Visual Studio, если разработка идёт под платформу .NET (Win).
  • -
- -Если не знаете, что выбрать -- можно посмотреть на них ;) - -## Не будем ссориться - -В списках выше перечислены редакторы, которые использую я или мои знакомые -- хорошие разработчики. Конечно, существуют и другие отличные редакторы, если вам что-то нравится -- пользуйтесь. - -Выбор редактора, как и любого инструмента, во многом индивидуален и зависит от ваших проектов, привычек, личных предпочтений. diff --git a/1-js/1-getting-started/4-devtools/article.md b/1-js/1-getting-started/4-devtools/article.md deleted file mode 100644 index 7a26dfce..00000000 --- a/1-js/1-getting-started/4-devtools/article.md +++ /dev/null @@ -1,114 +0,0 @@ -# Консоль разработчика - -При разработке скриптов всегда возможны ошибки... Впрочем, что я говорю? У вас абсолютно точно будут ошибки, если конечно вы -- человек, а не робот. - -Чтобы читать их в удобном виде, а также получать массу полезной информации о выполнении скриптов, в браузерах есть *инструменты разработки*. - -**Для разработки рекомендуется использовать Chrome или Firefox.** - -Другие браузеры, как правило, находятся в положении "догоняющих" по возможностям встроенных инструментов разработки. Если ошибка, к примеру, именно в Internet Explorer, тогда уже смотрим конкретно в нём, но обычно -- Chrome/Firefox. - -В инструментах разработчика предусмотрена масса возможностей, но на текущем этапе мы просто посмотрим, как их открывать, смотреть в консоли ошибки и запускать команды JavaScript. - -[cut] - -## Google Chrome - -Откройте страницу [bug.html](bug.html). - -В её JavaScript-коде есть ошибка. Конечно, обычному посетителю она не видна, нужно открыть инструменты разработчика. - -Для этого используйте сочетание клавиш [key Ctrl+Shift+J], а если у вас Mac, то [key Cmd+Shift+J]. - -При этом откроются инструменты разработчика и вкладка Console, в которой будет ошибка. - -Выглядеть будет примерно так: - - - - -
    -
  • При клике на `bug.html` вы перейдёте во вкладку с кодом к месту ошибки, там будет и краткое описание ошибки. -В данном случае ошибка вызвана строкой `lalala`, которая интерпретатору непонятна.
  • -
  • В этом же окошке вы можете набирать команды на JavaScript. Например, наберите `alert("Hello")` -- команду вывода сообщения и запустите её нажатием [key Enter]. Мы познакомимся с этой и другими командами далее.
  • -
  • Для перевода курсора на следующую строку (если команда состоит из нескольких строк) -- используется сочетание [key Shift+Enter].
  • -
- -Далее в учебнике мы подробнее рассмотрим отладку в Chrome в главе [](/debugging-chrome). - -## Firefox - -Для разработки в Firefox используется расширение Firebug. - -
    -
  1. Первым делом его надо установить. - -Это можно сделать со страницы https://addons.mozilla.org/ru/firefox/addon/firebug/. - -Перезапустите браузер. Firebug появится в правом-нижнем углу окна: - - - -Если иконки не видно -- возможно, у вас выключена панель расширений. Нажмите [key Ctrl+\] для её отображения. - -Ну а если её нет и там, то нажмите [key F12] -- это горячая клавиша для запуска Firebug, расширение появится, если установлено. -
  2. -
  3. Далее, для того чтобы консоль заработала, её надо включить. - -Если консоль уже была включена ранее, то этот шаг не нужен, но если она серая -- выберите в меню `Консоль` и включите её: - - - -
  4. -
  5. Для того, чтобы Firebug работал без глюков, желательно сначала открыть Firebug, а уже потом -- зайти на страницу. - -С открытым Firebug зайдите на страницу с ошибкой: [bug.html](/devtools/bug.html). - -Консоль покажет ошибку: - - - -Кликните на строчке с ошибкой и браузер покажет исходный код. При необходимости включайте дополнительные панели. -
  6. -
- -Как и в Chrome, можно набирать и запускать команды. Область для команд на рисунке находится справа, запуск команд осуществляется нажатием [key Ctrl+Enter] (для Mac -- [key Cmd+Enter]). - -Можно перенести её вниз, нажав на кнопочку (на рисунке её не видно, но она присутствует в правом нижнем углу панели разработки). - -Об основных возможностях можно прочитать на сайте firebug.ru. - -## Internet Explorer - -Панель разработчика запускается нажатием [key F12]. - -Откройте её и зайдите на страницу с ошибкой: [bug.html](/devtools/bug.html). Если вы разобрались с Chrome/Firefox, то дальнейшее будет вам более-менее понятно, так как инструменты IE построены позже и по аналогии с Chrome/Firefox. - -## Safari - -Горячие клавиши: [key Ctrl+Shift+I], [key Ctrl+Alt+C] для Mac -- [key Cmd] вместо [key Ctrl]. - -Для доступа к функционалу разработки через меню: - -
    -
  1. -В Safari первым делом нужно активировать меню разработки. - -Откройте меню, нажав на колесико справа-сверху и выберите `Настройки`. - -Затем вкладка `Дополнительно`: - - - -Отметьте `Показывать меню "Разработка" в строке меню`. Закройте настройки. -
  2. -
  3. Нажмите на колесико и выберите `Показать строку меню`. - -Инструменты будут доступны в появившейся строке меню, в пункте `Разработка`.
  4. -
- -## Итого - -Мы разобрали, как открывать инструменты разработчика и смотреть ошибки, а также запускать простые команды, не отходя от браузера. - -Далее мы приступим к изучению JavaScript. diff --git a/1-js/1-getting-started/4-devtools/bug.html b/1-js/1-getting-started/4-devtools/bug.html deleted file mode 100755 index b72b78f5..00000000 --- a/1-js/1-getting-started/4-devtools/bug.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - На этой странице есть скрипт с ошибкой. - - - - - \ No newline at end of file diff --git a/1-js/1-getting-started/4-devtools/chrome.png b/1-js/1-getting-started/4-devtools/chrome.png deleted file mode 100644 index b084d584..00000000 Binary files a/1-js/1-getting-started/4-devtools/chrome.png and /dev/null differ diff --git a/1-js/1-getting-started/4-devtools/chrome@2x.png b/1-js/1-getting-started/4-devtools/chrome@2x.png deleted file mode 100755 index 35fbb424..00000000 Binary files a/1-js/1-getting-started/4-devtools/chrome@2x.png and /dev/null differ diff --git a/1-js/1-getting-started/4-devtools/firebug-gray.png b/1-js/1-getting-started/4-devtools/firebug-gray.png deleted file mode 100755 index dd234fe0..00000000 Binary files a/1-js/1-getting-started/4-devtools/firebug-gray.png and /dev/null differ diff --git a/1-js/1-getting-started/4-devtools/firefox.png b/1-js/1-getting-started/4-devtools/firefox.png deleted file mode 100644 index 9ce9691c..00000000 Binary files a/1-js/1-getting-started/4-devtools/firefox.png and /dev/null differ diff --git a/1-js/1-getting-started/4-devtools/firefox@2x.png b/1-js/1-getting-started/4-devtools/firefox@2x.png deleted file mode 100755 index ff3633b7..00000000 Binary files a/1-js/1-getting-started/4-devtools/firefox@2x.png and /dev/null differ diff --git a/1-js/1-getting-started/4-devtools/firefox_console_down.png b/1-js/1-getting-started/4-devtools/firefox_console_down.png deleted file mode 100644 index 54cf92cd..00000000 Binary files a/1-js/1-getting-started/4-devtools/firefox_console_down.png and /dev/null differ diff --git a/1-js/1-getting-started/4-devtools/firefox_console_down@2x.png b/1-js/1-getting-started/4-devtools/firefox_console_down@2x.png deleted file mode 100755 index e19b9f3e..00000000 Binary files a/1-js/1-getting-started/4-devtools/firefox_console_down@2x.png and /dev/null differ diff --git a/1-js/1-getting-started/4-devtools/firefox_console_enable.png b/1-js/1-getting-started/4-devtools/firefox_console_enable.png deleted file mode 100644 index 848f2b82..00000000 Binary files a/1-js/1-getting-started/4-devtools/firefox_console_enable.png and /dev/null differ diff --git a/1-js/1-getting-started/4-devtools/firefox_console_enable@2x.png b/1-js/1-getting-started/4-devtools/firefox_console_enable@2x.png deleted file mode 100755 index 2bf6ea18..00000000 Binary files a/1-js/1-getting-started/4-devtools/firefox_console_enable@2x.png and /dev/null differ diff --git a/1-js/1-getting-started/4-devtools/safari.png b/1-js/1-getting-started/4-devtools/safari.png deleted file mode 100755 index 9a9cac5b..00000000 Binary files a/1-js/1-getting-started/4-devtools/safari.png and /dev/null differ diff --git a/1-js/1-getting-started/index.md b/1-js/1-getting-started/index.md deleted file mode 100644 index b3e8e77d..00000000 --- a/1-js/1-getting-started/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Введение - -Про язык JavaScript и окружение для разработки на нём. \ No newline at end of file diff --git a/1-js/10-es-modern/1-es-modern-usage/article.md b/1-js/10-es-modern/1-es-modern-usage/article.md deleted file mode 100644 index 86b8b7ff..00000000 --- a/1-js/10-es-modern/1-es-modern-usage/article.md +++ /dev/null @@ -1,75 +0,0 @@ -# ES-2015 сейчас - -[Стандарт ES-2015](http://www.ecma-international.org/publications/standards/Ecma-262.htm) был принят в июне 2015. Пока что большинство браузеров реализуют его частично, текущее состояние реализации различных возможностей можно посмотреть здесь: [](https://kangax.github.io/compat-table/es6/). - -Когда стандарт будет более-менее поддерживаться во всех браузерах, то весь учебник будет обновлён в соответствии с ним. Пока же, как центральное место для "сбора" современных фич JavaScript, создан этот раздел. - -Чтобы писать код на ES-2015 прямо сейчас, есть следующие варианты. - -## Конкретный движок JS - -Самое простое -- это когда нужен один конкретный движок JS, например V8 (Chrome). - -Тогда можно использовать только то, что поддерживается именно в нём. Заметим, что в V8 большинство возможностей ES-2015 поддерживаются только при включённом `use strict`. - -При разработке на Node.JS обычно так и делают. Если же нужна кросс-браузерная поддержка, то этот вариант не подойдёт. - -## Babel.JS - -[Babel.JS](https://babeljs.io) -- это [транспайлер](https://en.wikipedia.org/wiki/Source-to-source_compiler), переписывающий код на ES-2015 в код на предыдущем стандарте ES5. - -Он состоит из двух частей: - -
    -
  1. Собственно транспайлер, который переписывает код.
  2. -
  3. Полифилл, который добавляет методы `Array.from`, `String.prototype.repeat` и другие.
  4. -
- -На странице [](https://babeljs.io/repl/) можно поэкспериментировать с транспайлером: слева вводится код в ES-2015, а справа появляется результат его преобразования в ES5. - -Обычно Babel.JS работает на сервере в составе системы сборки JS-кода (например [webpack](http://webpack.github.io/) или [brunch](http://brunch.io/)) и автоматически переписывает весь код в ES5. - -Настройка такой конвертации тривиальна, единственно -- нужно поднять саму систему сборки, а добавить к ней Babel легко, плагины есть к любой из них. - -Если же хочется "поиграться", то можно использовать и браузерный вариант Babel. - -Это выглядит так: - -```html - -*!* - - -*/!* - - -``` - -Сверху подключается браузерный скрипт `browser.min.js` из пакета Babel. Он включает в себя полифилл и транспайлер. Далее он автоматически транслирует и выполняет скрипты с `type="text/babel"`. - -Размер `browser.min.js` превышает 1 мегабайт, поэтому такое использование в production строго не рекомендуется. - -# Примеры на этом сайте - -[warn header="Только при поддержке браузера"] -Запускаемые примеры с ES-2015 будут работать только если ваш браузер поддерживает соответствующую возможность стандарта. -[/warn] - -Это означает, что при запуске примеров в браузере, который их не поддерживает, будет ошибка. Это не означает, что пример неправильный! Просто пока нет поддержки... - -Рекомендуется [Chrome Canary](https://www.google.com/chrome/browser/canary.html) большинство примеров в нём работает. [Firefox Developer Edition](https://www.mozilla.org/en-US/firefox/channel/#developer) тоже неплох в поддержке современного стандарта, но на момент написания этого текста переменные [let](/let-const) работают только при указании `version=1.7` в типе скрипта: ` - - - - - \ No newline at end of file diff --git a/1-js/10-es-modern/11-promise/2-promise-sequence/source.view/guest.json b/1-js/10-es-modern/11-promise/2-promise-sequence/source.view/guest.json deleted file mode 100644 index c32ac845..00000000 --- a/1-js/10-es-modern/11-promise/2-promise-sequence/source.view/guest.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "guest", - "isAdmin": false -} diff --git a/1-js/10-es-modern/11-promise/2-promise-sequence/source.view/httpGet.js b/1-js/10-es-modern/11-promise/2-promise-sequence/source.view/httpGet.js deleted file mode 100644 index d7a16a96..00000000 --- a/1-js/10-es-modern/11-promise/2-promise-sequence/source.view/httpGet.js +++ /dev/null @@ -1,25 +0,0 @@ -function httpGet(url) { - - return new Promise(function(resolve, reject) { - - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - - xhr.onload = function() { - if (this.status == 200) { - resolve(this.response); - } else { - var error = new Error(this.statusText); - error.code = this.status; - reject(error); - } - }; - - xhr.onerror = function() { - reject(new Error("Network Error")); - }; - - xhr.send(); - }); - -} diff --git a/1-js/10-es-modern/11-promise/2-promise-sequence/source.view/index.html b/1-js/10-es-modern/11-promise/2-promise-sequence/source.view/index.html deleted file mode 100644 index fa875c5b..00000000 --- a/1-js/10-es-modern/11-promise/2-promise-sequence/source.view/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/10-es-modern/11-promise/2-promise-sequence/task.md b/1-js/10-es-modern/11-promise/2-promise-sequence/task.md deleted file mode 100644 index a75b8d9a..00000000 --- a/1-js/10-es-modern/11-promise/2-promise-sequence/task.md +++ /dev/null @@ -1,26 +0,0 @@ - -# Загрузить массив последовательно - -Есть массив URL: - -```js -//+ run -'use strict'; - -let urls = [ - 'user.json', - 'guest.json' -]; -``` - -Напишите код, который все URL из этого массива загружает -- один за другим (последовательно), и сохраняет в результаты в массиве `results`, а потом выводит. - -Вариант с параллельной загрузкой выглядел бы так: - -```js -Promise.all( urls.map(httpGet) ) - .then(alert); -``` - -В этой задаче загрузку нужно реализовать последовательно. - diff --git a/1-js/10-es-modern/11-promise/anon.png b/1-js/10-es-modern/11-promise/anon.png deleted file mode 100644 index a1675d2f..00000000 Binary files a/1-js/10-es-modern/11-promise/anon.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/article.md b/1-js/10-es-modern/11-promise/article.md deleted file mode 100644 index 5adb3687..00000000 --- a/1-js/10-es-modern/11-promise/article.md +++ /dev/null @@ -1,843 +0,0 @@ -# Promise - -Promise (обычно их так и называют "промисы") -- предоставляют удобный способ организации асинхронного кода. - -В современном JavaScript промисы часто используются в том числе и неявно, при помощи генераторов, но об этом чуть позже. - -## Что такое Promise? - -Promise -- это специальный объект, который содержит своё состояние. Вначале `pending` ("ожидание"), затем -- одно из: `fulfilled` ("выполнено успешно") или `rejected` ("выполнено с ошибкой"). - - - -На `promise` можно навешивать коллбэки двух типов: - -
    -
  • `onFulfilled` -- срабатывают, когда `promise` в состоянии "выполнен успешно".
  • -
  • `onRejected` -- срабатывают, когда `promise` в состоянии "выполнен с ошибкой".
  • -
- -Способ использования, в общих чертах, такой: -
    -
  1. Код, которому надо сделать что-то асинхронно, создаёт объект `promise` и возвращает его.
  2. -
  3. Внешний код, получив `promise`, навешивает на него обработчики.
  4. -
  5. По завершении процесса асинхронный код переводит `promise` в состояние `fulfilled` (с результатом) или `rejected` (с ошибкой). При этом автоматически вызываются соответствующие обработчики во внешнем коде.
  6. -
- -Синтаксис создания `Promise`: - -```js -var promise = new Promise(function(resolve, reject) { - // Эта функция будет вызвана автоматически - - // В ней можно делать любые асинхронные операции, - // А когда они завершатся — нужно вызвать одно из: - // resolve(результат) при успешном выполнении - // reject(ошибка) при ошибке -}) -``` - -Универсальный метод для навешивания обработчиков: - -```js -promise.then(onFulfilled, onRejected) -``` - -
    -
  • `onFulfilled` -- функция, которая будет вызвана с результатом при `resolve`.
  • -
  • `onRejected` -- функция, которая будет вызвана с ошибкой при `reject`.
  • -
- -С его помощью можно назначить как оба обработчика сразу, так и только один: - -```js -// onFulfilled сработает при успешном выполнении -promise.then(onFulfilled) -// onRejected сработает при ошибке -promise.then(null, onRejected) -``` - -[smart header=".catch"] -Для того, чтобы поставить обработчик только на ошибку, вместо `.then(null, onRejected)` можно написать `.catch(onRejected)` -- это то же самое. -[/smart] - -[smart header="Синхронный `throw` -- то же самое, что `reject`"] -Если в функции промиса происходит синхронный `throw` (или иная ошибка), то вызывается `reject`: -```js -//+ run -'use strict'; - -let p = new Promise((resolve, reject) => { - // то же что reject(new Error("o_O")) - throw new Error("o_O"); -}) - -p.catch(alert); // Error: o_O -``` -[/smart] - -Посмотрим, как это выглядит вместе, на простом примере. - - -## Пример с setTimeout - -Возьмём `setTimeout` в качестве асинхронной операции, которая должна через некоторое время успешно завершиться с результатом "result": - -```js -//+ run -'use strict'; - -// Создаётся объект promise -let promise = new Promise((resolve, reject) => { - - setTimeout(() => { - // переведёт промис в состояние fulfilled с результатом "result" - resolve("result"); - }, 1000); - -}); - -// promise.then навешивает обработчики на успешный результат или ошибку -promise - .then( - result => { - // первая функция-обработчик - запустится при вызове resolve - alert("Fulfilled: " + result); // result - аргумент resolve - }, - error => { - // вторая функция - запустится при вызове reject - alert("Rejected: " + error); // error - аргумент reject - } - ); - -``` - -В результате запуска кода выше -- через 1 секунду выведется "Fulfilled: result". - -А если бы вместо `resolve("result")` был вызов `reject("error")`, то вывелось бы "Rejected: error". Впрочем, как правило, если при выполнении возникла проблема, то `reject` вызывают не со строкой, а с объектом ошибки типа `new Error`: - -```js -//+ run -// Этот promise завершится с ошибкой через 1 секунду -var promise = new Promise((resolve, reject) => { - - setTimeout(() => { -*!* - reject(new Error("время вышло!")); -*/!* - }, 1000); - -}); - -promise - .then( - result => alert("Fulfilled: " + result), -*!* - error => alert("Rejected: " + error.message) // Rejected: время вышло! -*/!* - ); - -``` - -Конечно, вместо `setTimeout` внутри функции промиса может быть и запрос к серверу и ожидание ввода пользователя, или другой асинхронный процесс. Главное, чтобы по своему завершению он вызвал `resolve` или `reject`, которые передадут результат обработчикам. - -[smart header="Только один аргумент"] -Функции `resolve/reject` принимают ровно один аргумент -- результат/ошибку. - -Именно он передаётся обработчикам в `.then`, как можно видеть в примерах выше. -[/smart] - -## Promise после reject/resolve -- неизменны - -Заметим, что после вызова `resolve/reject` промис уже не может "передумать". - -Когда промис переходит в состояние "выполнен" -- с результатом (resolve) или ошибкой (reject) -- это навсегда. - -Например: - -```js -//+ run -'use strict'; - -let promise = new Promise((resolve, reject) => { - -*!* - // через 1 секунду готов результат: result -*/!* - setTimeout(() => resolve("result"), 1000); - -*!* - // через 2 секунды — reject с ошибкой, он будет проигнорирован -*/!* - setTimeout(() => reject(new Error("ignored")), 2000); - -}); - -promise - .then( - result => alert("Fulfilled: " + result), // сработает - error => alert("Rejected: " + error) // не сработает - ); - -``` - -В результате вызова этого кода сработает только первый обработчик `then`, так как после вызова `resolve` промис уже получил состояние (с результатом), и в дальнейшем его уже ничто не изменит. - -Последующие вызовы resolve/reject будут просто проигнороированы. - -А так -- наоборот, ошибка будет раньше: - - -```js -//+ run -'use strict'; - -let promise = new Promise((resolve, reject) => { - - // reject вызван раньше, resolve будет проигнорирован - setTimeout(() => reject(new Error("error")), 1000); - - setTimeout(() => resolve("ignored"), 2000); - -}); - -promise - .then( - result => alert("Fulfilled: " + result), // не сработает - error => alert("Rejected: " + error) // сработает - ); - -``` - - -## Промисификация - -*Промисификация* -- это когда берут асинхронный функционал и делают для него обёртку, возвращающую промис. - -После промисификации использование функционала зачастую становится гораздо удобнее. - -В качестве примера сделаем такую обёртку для запросов при помощи XMLHttpRequest. - -Функция `httpGet(url)` будет возвращать промис, который при успешной загрузке данных с `url` будет переходить в `fulfilled` с этими данными, а при ошибке -- в `rejected` с информацией об ошибке: - -```js -//+ autorun -function httpGet(url) { - - return new Promise(function(resolve, reject) { - - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - - xhr.onload = function() { - if (this.status == 200) { -*!* - resolve(this.response); -*/!* - } else { -*!* - var error = new Error(this.statusText); - error.code = this.status; - reject(error); -*/!* - } - }; - - xhr.onerror = function() { -*!* - reject(new Error("Network Error")); -*/!* - }; - - xhr.send(); - }); - -} -``` - -Как видно, внутри функции объект `XMLHttpRequest` создаётся и отсылается как обычно, при `onload/onerror` вызываются, соответственно, `resolve` (при статусе 200) или `reject`. - -Использование: - -```js -//+ run -httpGet("/article/promise/user.json") - .then( - response => alert(`Fulfilled: ${response}`), - error => alert(`Rejected: ${error}`) - ); -``` - - -[smart header="Метод `fetch`"] -Заметим, что ряд современных браузеров уже поддерживает [fetch](/fetch) -- новый встроенный метод для AJAX-запросов, призванный заменить XMLHttpRequest. Он гораздо мощнее, чем `httpGet`. И -- да, этот метод использует промисы. Полифилл для него доступен на [](https://github.com/github/fetch). -[/smart] - - -## Цепочки промисов - -"Чейнинг" (chaining), то есть возможность строить асинхронные цепочки из промисов -- пожалуй, основная причина, из-за которой существуют и активно используются промисы. - -Например, мы хотим по очереди: -
    -
  1. Загрузить данные посетителя с сервера (асинхронно).
  2. -
  3. Затем отправить запрос о нём на github (асинхронно).
  4. -
  5. Когда это будет готово, вывести его github-аватар на экран (асинхронно).
  6. -
  7. ...И сделать код расширяемым, чтобы цепочку можно было легко продолжить.
  8. -
- -Вот код для этого, использующий функцию `httpGet`, описанную выше: - -```js -//+ run -'use strict'; - -// сделать запрос -httpGet('/article/promise/user.json') -*!* - // 1. Получить данные о пользователе в JSON и передать дальше -*/!* - .then(response => { - console.log(response); - let user = JSON.parse(response); -*!* - return user; -*/!* - }) -*!* - // 2. Получить информацию с github -*/!* - .then(user => { - console.log(user); -*!* - return httpGet(`https://api.github.com/users/${user.name}`); -*/!* - }) -*!* - // 3. Вывести аватар на 3 секунды (можно с анимацией) -*/!* - .then(githubUser => { - console.log(githubUser); - githubUser = JSON.parse(githubUser); - - let img = new Image(); - img.src = githubUser.avatar_url; - img.className = "promise-avatar-example"; - document.body.appendChild(img); - -*!* - setTimeout(() => img.remove(), 3000); // (*) -*/!* - }); -``` - -Самое главное в этом коде -- последовательность вызовов: - -```js -httpGet(...) - .then(...) - .then(...) - .then(...) -``` - -При чейнинге, то есть последовательных вызовах `.then…then…then`, в каждый следующий `then` переходит результат от предыдущего. Вызовы `console.log` оставлены, чтобы при запуске можно было посмотреть конкретные значения, хотя они здесь и не очень важны. - -**Если очередной `then` вернул промис, то далее по цепочке будет передан не сам этот промис, а его результат.** - -В коде выше: - -
    -
  1. В первом `then` возвращается объект `user`, он переходит в следующий `then`.
  2. -
  3. Во втором `then` возвращается промис (результат нового вызова `httpGet`). Когда он будет завершён (может пройти какое-то время), то будет вызван следующий `then`.
  4. -
  5. Третий `then` ничего не возвращает.
  6. -
- -Схематично его работу можно изобразить так: - - - -Значком "песочные часы" помечены периоды ожидания, которых всего два: в исходном `httpGet` и в подвызове далее по цепочке. - -Если `then` возвращает промис, то до его выполнения может пройти некоторое время, оставшаяся часть цепочки будет ждать. - -То есть, логика довольно проста: -
    -
  • В каждом `then` мы получаем текущий результат работы.
  • -
  • Можно его обработать синхронно и вернуть результат (например, применить `JSON.parse`). Или же, если нужна асинхронная обработка -- инициировать её и вернуть промис.
  • -
- -Обратим внимание, что последний `then` в нашем примере ничего не возвращает. Если мы хотим, чтобы после `setTimeout` `(*)` асинхронная цепочка могла быть продолжена, то последний `then` тоже должен вернуть промис. Это общее правило: если внутри `then` стартует новый асинхронный процесс, то для того, чтобы оставшаяся часть цепочки выполнилась после его окончания, мы должны вернуть промис. - -В данном случае промис должен перейти в состояние "выполнен" после срабатывания `setTimeout`. - -Строку `(*)` для этого нужно переписать так: -```js -.then(githubUser => { - ... - - // вместо setTimeout(() => img.remove(), 3000); (*) - - return new Promise((resolve, reject) => { - setTimeout(() => { - img.remove(); - // после таймаута — вызов resolve, - // можно без результата, чтобы управление перешло в следующий then - // (или можно передать данные пользователя дальше по цепочке) -*!* - resolve(); -*/!* - }, 3000); - }); -}) -``` - -Теперь, если к цепочке добавить ещё `then`, то он будет вызван после окончания `setTimeout`. - -## Перехват ошибок - -Выше мы рассмотрели "идеальный случай" выполнения, когда ошибок нет. - -А что, если github не отвечает? Или JSON.parse бросил синтаксическую ошибку при обработке данных? - -Да мало ли, где ошибка... - -Правило здесь очень простое. - -**При возникновении ошибки -- она отправляется в ближайший обработчик `onRejected`.** - -Такой обработчик нужно поставить через второй аргумент `.then(..., onRejected)` или, что то же самое, через `.catch(onRejected)`. - -Чтобы поймать всевозможные ошибки, которые возникнут при загрузке и обработке данных, добавим `catch` в конец нашей цепочки: - -```js -//+ run -'use strict'; - -*!* -// в httpGet обратимся к несуществующей странице -*/!* -httpGet('/page-not-exists') - .then(response => JSON.parse(response)) - .then(user => httpGet(`https://api.github.com/users/${user.name}`)) - .then(githubUser => { - githubUser = JSON.parse(githubUser); - - let img = new Image(); - img.src = githubUser.avatar_url; - img.className = "promise-avatar-example"; - document.body.appendChild(img); - - return new Promise((resolve, reject) => { - setTimeout(() => { - img.remove(); - resolve(); - }, 3000); - }); - }) -*!* - .catch(error => { - alert(error); // Error: Not Found - }); -*/!* -``` - -В примере выше ошибка возникает в первом же `httpGet`, но `catch` с тем же успехом поймал бы ошибку во втором `httpGet` или в `JSON.parse`. - -Принцип очень похож на обычный `try..catch`: мы делаем асинхронную цепочку из `.then`, а затем, когда нужно перехватить ошибки, вызываем `.catch(onRejected)`. - - -[smart header="А что после `catch`?"] -Обработчик `.catch(onRejected)` получает ошибку и должен обработать её. - -Есть два варианта развития событий: -
    -
  1. Если ошибка не критичная, то `onRejected` возвращает значение через `return`, и управление переходит в ближайший `.then(onFulfilled)`.
  2. -
  3. Если продолжить выполнение с такой ошибкой нельзя, то он делает `throw`, и тогда ошибка переходит в следующий ближайший `.catch(onRejected)`. -
  4. -
- -Это также похоже на обычный `try..catch` -- в блоке `catch` ошибка либо обрабатывается, и тогда выполнение кода продолжается как обычно, либо он делает `throw`. Существенное отличие -- в том, что промисы асинхронные, поэтому при отсутствии внешнего `.catch` ошибка не "вываливается" в консоль и не "убивает" скрипт. - -Ведь возможно, что новый обработчик `.catch` будет добавлен в цепочку позже. -[/smart] - -## Промисы в деталях - -Самым основным источником информации по промисам является, разумеется, [стандарт](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-promise-objects). - -Чтобы наше понимание промисов было полным, и мы могли с лёгкостью разрешать сложные ситуации, посмотрим внимательнее, что такое промис и как он работает, но уже не в общих словах, а детально, в соответствии со стандартом EcmaScript. - -Согласно стандарту, у объекта `new Promise(executor)` при создании есть четыре внутренних свойства: - -
    -
  • `PromiseState` -- состояние, вначале "pending".
  • -
  • `PromiseResult` -- результат, при создании значения нет.
  • -
  • `PromiseFulfillReactions` -- список функций-обработчиков успешного выполнения.
  • -
  • `PromiseRejectReactions` -- список функций-обработчиков ошибки.
  • -
- - - -Когда функция-executor вызывает `reject` или `resolve`, то `PromiseState` становится `"resolved"` или `"rejected"`, а все функции-обработчики из соответствующего списка перемещаются в специальную системную очередь `"PromiseJobs"`. - -Эта очередь автоматически выполняется, когда интерпретатору "нечего делать". Иначе говоря, все функции-обработчики выполнятся асинхронно, одна за другой, по завершении текущего кода, примерно как `setTimeout(..,0)`. - -Исключение из этого правила -- если `resolve` возвращает другой `Promise`. Тогда дальнейшее выполнение ожидает его результата (в очередь помещается специальная задача), и функции-обработчики выполняются уже с ним. - -Добавляет обработчики в списки один метод: `.then(onResolved, onRejected)`. Метод `.catch(onRejected)` -- всего лишь сокращённая запись `.then(null, onRejected)`. - -Он делает следующее: -
    -
  • Если `PromiseState == "pending"`, то есть промис ещё не выполнен, то обработчики добавляются в соответствующие списки.
  • -
  • Иначе обработчики сразу помещаются в очередь на выполнение.
  • -
- -Здесь важно, что обработчики можно добавлять в любой момент. Можно до выполнения промиса (они подождут), а можно -- после (выполнятся в ближайшее время, через асинхронную очередь). - -Например: - -```js -//+ run -// Промис выполнится сразу же -var promise = new Promise((resolve, reject) => resolve(1)); - -// PromiseState = "resolved" -// PromiseResult = 1 - -// Добавили обработчик к выполненному промису -promise.then(alert); // ...он сработает тут же -``` - -Разумеется, можно добавлять и много обработчиков на один и тот же промис: - - -```js -//+ run -// Промис выполнится сразу же -var promise = new Promise((resolve, reject) => resolve(1)); - -promise.then( function f1(result) { -*!* - alert(result); // 1 -*/!* - return 'f1'; -}) - -promise.then( function f2(result) { -*!* - alert(result); // 1 -*/!* - return 'f2'; -}) -``` - -Вид объекта `promise` после этого: - - - -На этой иллюстрации можно увидеть добавленные нами обработчики `f1`, `f2`, а также -- автоматические добавленные обработчики ошибок `"Thrower"`. - -Дело в том, что `.then`, если один из обработчиков не указан, добавляет его "от себя", следующим образом: -
    -
  • Для успешного выполнения -- функция `Identity`, которая выглядит как `arg => return arg`, то есть возвращает аргумент без изменений.
  • -
  • Для ошибки -- функция `Thrower`, которая выглядит как `arg => throw arg`, то есть генерирует ошибку.
  • -
- -Это, по сути дела, формальность, но без неё некоторые особенности поведения промисов могут "не сойтись" в общую логику, поэтому мы упоминаем о ней здесь. - -Обратим внимание, в этом примере намеренно *не используется чейнинг*. То есть, обработчики добавляются именно на один и тот же промис. - -Поэтому оба `alert` выдадут одно значение `1`. - -Все функции из списка обработчиков вызываются с результатом промиса, одна за другой. Никакой передачи результатов между обработчиками в рамках одного промиса нет, а сам результат промиса (`PromiseResult`) после установки не меняется. - -Поэтому, чтобы продолжить работу с результатом, используется чейнинг. - -**Для того, чтобы результат обработчика передать следующей функции, `.then` создаёт новый промис и возвращает его.** - -В примере выше создаётся два таких промиса (т.к. два вызова `.then`), каждый из которых даёт свою ветку выполнения: - - - -Изначально эти новые промисы -- "пустые", они ждут. Когда в будущем выполнятся обработчики `f1, f2`, то их результат будет передан в новые промисы по стандартному принципу: - -
    -
  • Если вернётся обычное значение (не промис), новый промис перейдёт в `"resolved"` с ним.
  • -
  • Если был `throw`, то новый промис перейдёт в состояние `"rejected"` с ошибкой.
  • -
  • Если вернётся промис, то используем его результат (он может быть как `resolved`, так и `rejected`).
  • -
- - - -Дальше выполнятся уже обработчики на новом промисе, и так далее. - -Чтобы лучше понять происходящее, посмотрим на цепочку, которая получается в процессе написания кода для показа github-аватара. - -Первый промис и обработка его результата: - -```js -httpGet('/article/promise/user.json') - .then(JSON.parse) -``` - - - - -Если промис завершился через `resolve`, то результат -- в `JSON.parse`, если `reject` -- то в Thrower. - -Как было сказано выше, `Thrower` -- это стандартная внутренняя функция, которая автоматически используется, если второй обработчик не указан. - -Можно считать, что второй обработчик выглядит так: - -```js -httpGet('/article/promise/user.json') - .then(JSON.parse, *!*err => throw err*/!*) -``` - -Заметим, что когда обработчик в промисах делает `throw` -- в данном случае, при ошибке запроса, то такая ошибка не "валит" скрипт и не выводится в консоли. Она просто будет передана в ближайший следующий обработчик `onRejected`. - -Добавим в код ещё строку: - -```js -httpGet('/article/promise/user.json') - .then(JSON.parse) -*!* - .then(user => httpGet(`https://api.github.com/users/${user.name}`)) -*/!* -``` - -Цепочка "выросла вниз": - - - -Функция `JSON.parse` либо возвращает объект с данными, либо генерирует ошибку (что расценивается как `reject`). - -Если всё хорошо, то `then(user => httpGet(…))` вернёт новый промис, на который стоят уже два обработчика: - - -```js -httpGet('/article/promise/user.json') - .then(JSON.parse) - .then(user => httpGet(`https://api.github.com/users/${user.name}`)) - .then( -*!* - JSON.parse, - function avatarError(error) { - if (error.code == 404) { - return {name: "NoGithub", avatar_url: '/article/promise/anon.png'}; - } else { - throw error; - } - } -*/!* - }) -``` - - - -Наконец-то хоть какая-то обработка ошибок! - -Обработчик `avatarError` перехватит ошибки, которые были ранее. Функция `httpGet` при генерации ошибки записывает её HTTP-код в свойство `error.code`, так что мы легко можем понять -- что это: - -
    -
  • Если страница на Github не найдена -- можно продолжить выполнение, используя "аватар по умолчанию"
  • -
  • Иначе -- пробрасываем ошибку дальше.
  • -
- -Итого, после добавления оставшейся части цепочки, картина получается следующей: - -```js -//+ run -'use strict'; - -httpGet('/article/promise/userNoGithub.json') - .then(JSON.parse) - .then(user => loadUrl(`https://api.github.com/users/${user.name}`)) - .then( - JSON.parse, - function githubError(error) { - if (error.code == 404) { - return {name: "NoGithub", avatar_url: '/article/promise/anon.png'}; - } else { - throw error; - } - } - }) - .then(function showAvatar(githubUser) { - let img = new Image(); - img.src = githubUser.avatar_url; - img.className = "promise-avatar-example"; - document.body.appendChild(img); - setTimeout(() => img.remove(), 3000); - }) - .catch(function genericError(error) { - alert(error); // Error: Not Found - }); -``` - - - -В конце срабатывает общий обработчик `genericError`, который перехватывает любые ошибки. В данном случае ошибки, которые в него попадут, уже носят критический характер, что-то серьёзно не так. Чтобы посетитель не удивился отсутствию информации, мы показываем ему сообщение об этом. - -Можно и как-то иначе вывести уведомление о проблеме, главное -- не забыть обработать ошибки в конце. Если последнего `catch` не будет, а цепочка завершится с ошибкой, то посетитель об этом не узнает. - -В консоли тоже ничего не будет, так как ошибка остаётся "внутри" промиса, ожидая добавления следующего обработчика `onRejected`, которому будет передана. - -Итак, мы рассмотрели основные приёмы использования промисов. Далее -- посмотрим некоторые полезные вспомогательные методы. - -## Параллельное выполнение - -Что, если мы хотим осуществить несколько асинхронных процессов одновременно и обработать их результат? - - -В классе `Promise` есть следующие статические методы. - -### Promise.all(iterable) - -Вызов `Promise.all(iterable)` получает массив (или другой итерируемый объект) промисов и возвращает промис, который ждёт, пока все переданные промисы завершатся, и переходит в состояние "выполнено" с массивом их результатов. - -Например: - -```js -//+ run - -Promise.all([ - httpGet('/article/promise/user.json'), - httpGet('/article/promise/guest.json') -]).then(results => { - alert(results); -}); -``` - -Допустим, у нас есть массив с URL. - -```js -let urls = [ - '/article/promise/user.json', - '/article/promise/guest.json' -]; -``` - -Чтобы загрузить их параллельно, нужно: -
    -
  1. Создать для каждого URL соответствующий промис.
  2. -
  3. Обернуть массив таких промисов в `Promise.all`.
  4. -
- -Получится так: - -```js -//+ run -'use strict'; - -let urls = [ - '/article/promise/user.json', - '/article/promise/guest.json' -]; - -*!* -Promise.all( urls.map(httpGet) ) -*/!* - .then(results => { - alert(results); - }); -``` - -Заметим, что если какой-то из промисов завершился с ошибкой, то результатом `Promise.all` будет эта ошибка. При этом остальные промисы игнорируются. - -Например: - - -```js -//+ run - -Promise.all([ - httpGet('/article/promise/user.json'), - httpGet('/article/promise/guest.json'), - httpGet('/article/promise/no-such-page.json') // (нет такой страницы) -]).then( - result => alert("не сработает"), - error => alert("Ошибка: " + error.message) // Ошибка: Not Found -) -``` - -### Promise.race(iterable) - -Вызов `Promise.race`, как и `Promise.all`, получает итерируемый объект с промисами, которые нужно выполнить, и возвращает новый промис. - -Но, в отличие от `Promise.all`, результатом будет только первый успешно выполнившийся промис из списка. Остальные игнорируются. - -Например: - -```js -//+ run - -Promise.race([ - httpGet('/article/promise/user.json'), - httpGet('/article/promise/guest.json') -]).then(firstResult => { - firstResult = JSON.parse(firstResult); - alert( firstResult.name ); // iliakan или guest, смотря что загрузится раньше -}); -``` - -## Promise.resolve(value) - -Вызов `Promise.resolve(value)` создаёт успешно выполнившийся промис с результатом `value`. - -Он аналогичен конструкции: - -```js -new Promise((resolve) => resolve(value)) -``` - -`Promise.resolve`, когда хотят построить асинхронную цепочку, и начальный результат уже есть. - - -Например: - -```js -//+ run -Promise.resolve(window.location) // начать с этого значения - .then(httpGet) // вызвать для него httpGet - .then(alert) // и вывести результат -``` - -## Promise.reject(error) - -Аналогично `Promise.resolve(value)` создаёт уже выполнившийся промис, но не с успешным результатом, а с ошибкой `error`. - -Например: - -```js -//+ run -Promise.reject(new Error("...")) - .catch(alert) // Error: ... -``` - -Метод `Promise.reject` используется очень редко, гораздо реже чем `resolve`, потому что ошибка возникает обычно не в начале цепочки, а в процессе её выполнения. - -## Итого - -
    -
  • Промис -- это специальный объект, который хранит своё состояние, текущий результат (если есть) и коллбэки.
  • -
  • При создании `new Promise((resolve, reject) => ...)` автоматически запускается функция-аргумент, которая должна вызвать `resolve(result)` при успешном выполнении и `reject(error)` -- при ошибке.
  • -
  • Аргумент `resolve/reject` (только первый, остальные игнорируются) передаётся обработчикам на этом промисе.
  • -
  • Обработчики назначаются вызовом `.then/catch`.
  • -
  • Для передачи результата от одного обработчика к другому используется чейнинг.
  • -
- -У промисов есть некоторые ограничения. В частности, стандарт не предусматривает какой-то метод для "отмены" промиса, хотя в ряде ситуаций (http-запросы) это было бы довольно удобно. Возможно, он появится в следующей версии стандарта JavaScript. - -В современной JavaScript-разработке сложные цепочки с промисами используются редко, так как они куда проще описываются при помощи генераторов с библиотекой `co`, которые рассмотрены в [соответствующей главе](/generator). Можно сказать, что промисы лежат в основе более продвинутых способов асинхронной разработки. - -[head] - -[/head] diff --git a/1-js/10-es-modern/11-promise/guest.json b/1-js/10-es-modern/11-promise/guest.json deleted file mode 100644 index c32ac845..00000000 --- a/1-js/10-es-modern/11-promise/guest.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "guest", - "isAdmin": false -} diff --git a/1-js/10-es-modern/11-promise/promiseEcma.png b/1-js/10-es-modern/11-promise/promiseEcma.png deleted file mode 100644 index f3ffe574..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseEcma.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseEcma@2x.png b/1-js/10-es-modern/11-promise/promiseEcma@2x.png deleted file mode 100644 index f1a801a9..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseEcma@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseHandlerVariants.png b/1-js/10-es-modern/11-promise/promiseHandlerVariants.png deleted file mode 100644 index 4161d9f5..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseHandlerVariants.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseHandlerVariants@2x.png b/1-js/10-es-modern/11-promise/promiseHandlerVariants@2x.png deleted file mode 100644 index f4130ca6..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseHandlerVariants@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseInit.png b/1-js/10-es-modern/11-promise/promiseInit.png deleted file mode 100644 index 148b4b8d..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseInit.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseInit@2x.png b/1-js/10-es-modern/11-promise/promiseInit@2x.png deleted file mode 100644 index aabb57b7..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseInit@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-1.png b/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-1.png deleted file mode 100644 index 58847d65..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-1.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-1@2x.png b/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-1@2x.png deleted file mode 100644 index 9b4ae76b..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-1@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-2.png b/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-2.png deleted file mode 100644 index ebd35699..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-2.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-2@2x.png b/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-2@2x.png deleted file mode 100644 index e9b73840..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-2@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-3.png b/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-3.png deleted file mode 100644 index 3e9056a7..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-3.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-3@2x.png b/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-3@2x.png deleted file mode 100644 index 3c461f7e..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-3@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-4.png b/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-4.png deleted file mode 100644 index d2d1d3a5..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-4.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-4@2x.png b/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-4@2x.png deleted file mode 100644 index d3bc720d..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain-4@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain.png b/1-js/10-es-modern/11-promise/promiseLoadAvatarChain.png deleted file mode 100644 index ea162ad1..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain@2x.png b/1-js/10-es-modern/11-promise/promiseLoadAvatarChain@2x.png deleted file mode 100644 index 1e5f23bb..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseLoadAvatarChain@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseTwo.png b/1-js/10-es-modern/11-promise/promiseTwo.png deleted file mode 100644 index 0600ba4d..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseTwo.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseTwo@2x.png b/1-js/10-es-modern/11-promise/promiseTwo@2x.png deleted file mode 100644 index 95e4e749..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseTwo@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseTwoThen.png b/1-js/10-es-modern/11-promise/promiseTwoThen.png deleted file mode 100644 index 439b4d85..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseTwoThen.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseTwoThen@2x.png b/1-js/10-es-modern/11-promise/promiseTwoThen@2x.png deleted file mode 100644 index db5be298..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseTwoThen@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseUserFlow.png b/1-js/10-es-modern/11-promise/promiseUserFlow.png deleted file mode 100644 index 110ce79b..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseUserFlow.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/promiseUserFlow@2x.png b/1-js/10-es-modern/11-promise/promiseUserFlow@2x.png deleted file mode 100644 index a185fb73..00000000 Binary files a/1-js/10-es-modern/11-promise/promiseUserFlow@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/11-promise/user.json b/1-js/10-es-modern/11-promise/user.json deleted file mode 100644 index 32f89971..00000000 --- a/1-js/10-es-modern/11-promise/user.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "iliakan", - "isAdmin": true -} diff --git a/1-js/10-es-modern/11-promise/userNoGithub.json b/1-js/10-es-modern/11-promise/userNoGithub.json deleted file mode 100644 index 9e23f32b..00000000 --- a/1-js/10-es-modern/11-promise/userNoGithub.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "an-unknown-person-32662", - "isAdmin": false -} diff --git a/1-js/10-es-modern/12-generator/anon.png b/1-js/10-es-modern/12-generator/anon.png deleted file mode 100644 index a1675d2f..00000000 Binary files a/1-js/10-es-modern/12-generator/anon.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/article.md b/1-js/10-es-modern/12-generator/article.md deleted file mode 100644 index b7b25d1d..00000000 --- a/1-js/10-es-modern/12-generator/article.md +++ /dev/null @@ -1,904 +0,0 @@ - -# Генераторы - -Генераторы -- новый вид функций в современном JavaScript. Они отличаются от обычных тем, что могут приостанавливать своё выполнение, возвращать промежуточный результат и далее возобновлять его позже, в произвольный момент времени. - -## Создание генератора - -Для объявления генератора используется новая синтаксическая конструкция: `function*` (функция со звёздочкой). - -Её называют "функция-генератор" (generator function). - -Выглядит это так: - -```js -function* generateSequence() { - yield 1; - yield 2; - return 3; -} -``` - -При запуске `generateSequence()` код такой функции не выполняется. Вместо этого она возвращает специальный объект, который как раз и называют "генератором". - -```js -// generator function создаёт generator -let generator = generateSequence(); -``` - -Правильнее всего будет воспринимать генератор как "замороженный вызов функции": - - - -При создании генератора код находится в начале своего выполнения. - -Основным методом генератора является `next()`. При вызове он возобновляет выполнение кода до ближайшего ключевого слова `yield`. По достижении `yield` выполнение приостанавливается, а значение -- возвращается во внешний код: - -```js -//+ run -'use strict'; - -function* generateSequence() { - yield 1; - yield 2; - return 3; -} - -let generator = generateSequence(); - -*!* -let one = generator.next(); -*/!* - -alert(JSON.stringify(one)); // {value: 1, done: false} -``` - - - -Повторный вызов `generator.next()` возобновит выполнение и вернёт результат следующего `yield`: - -```js -let two = generator.next(); - -alert(JSON.stringify(two)); // {value: 2, done: false} -``` - - - -И, наконец, последний вызов завершит выполнение функции и вернёт результат `return`: - -```js -let three = generator.next(); - -alert(JSON.stringify(three)); // {value: 3, *!*done: true*/!*} -``` - - - - -Функция завершена. Внешний код должен увидеть это из свойства `done:true` и обработать `value:3`, как окончательный результат. - -Новые вызовы `generator.next()` больше не имеют смысла. Впрочем, если они и будут, то не вызовут ошибки, но будут возвращать один и тот же объект: `{done: true}`. - -"Открутить назад" завершившийся генератор нельзя, но можно создать новый ещё одним вызовом `generateSequence()` и выполнить его. - -[smart header="`function* (…)` или `function *(…)`?"] -Можно ставить звёздочку как сразу после `function`, так и позже, перед названием. В интернет можно найти обе эти формы записи, они верны: -```js -function* f() { - // звёздочка после function -} - -function *f() { - // звёздочка перед названием -} -``` - -Технически, нет разницы, но писать то так то эдак -- довольно странно, надо остановиться на чём-то одном. - -Автор этого текста полагает, что правильнее использовать первый вариант `function*`, так как звёздочка относится к типу объявляемой сущности (`function*` -- "функция-генератор"), а не к её названию. Конечно, это всего лишь рекомендация-мнение, не обязательное к выполнению, работать будет в любом случае. -[/smart] - - - - -## Генератор -- итератор - -Как вы, наверно, уже догадались по наличию метода `next()`, генератор связан с [итераторами](/iterator). В частности, он является итерируемым объектом. - -Его можно перебирать и через `for..of`: - -```js -//+ run -'use strict'; - -function* generateSequence() { - yield 1; - yield 2; - return 3; -} - -let generator = generateSequence(); - -for(let value of generator) { - alert(value); // 1, затем 2 -} -``` - -Заметим, однако, существенную особенность такого перебора! - -При запуске примера выше будет выведено значение `1`, затем `2`. Значение `3` выведено не будет. Это потому что стандартные перебор итератора игнорирует `value` на последнем значении, при `done: true`. Так что результат `return` в цикле `for..of` не выводится. - -Соответственно, если мы хотим, чтобы все значения возвращались при переборе через `for..of`, то надо возвращать их через `yield`: - - -```js -//+ run -'use strict'; - -function* generateSequence() { - yield 1; - yield 2; -*!* - yield 3; -*/!* -} - -let generator = generateSequence(); - -for(let value of generator) { - alert(value); // 1, затем 2, затем 3 -} -``` - -...А зачем вообще `return` при таком раскладе, если его результат игнорируется? Он тоже нужен, но в других ситуациях. Перебор через `for..of` -- в некотором смысле "исключение". Как мы увидим дальше, в других контекстах `return` очень даже востребован. - -## Композиция генераторов - -Один генератор может включать в себя другие. Это называется композицией. - -Разберём композицию на примере. - -Пусть у нас есть функция `generateSequence`, которая генерирует последовательность чисел: - -```js -//+ run -'use strict'; - -function* generateSequence(start, end) { - - for (let i = start; i <= end; i++) { - yield i; - } - -} - -// Используем оператор … для преобразования итерируемого объекта в массив -let sequence = [...generateSequence(2,5)]; - -alert(sequence); // 2, 3, 4, 5 -``` - -Мы хотим на её основе сделать другую функцию `generateAlphaNumCodes()`, которая будет генерировать коды для буквенно-цифровых символов латинского алфавита: - -
    -
  • `48..57` -- для `0..9`
  • -
  • `65..90` -- для `A..Z`
  • -
  • `97..122` -- для `a..z`
  • -
- -Далее этот набор кодов можно превратить в строку и использовать, к примеру, для выбора из него случайного пароля. Только символы пунктуации ещё хорошо бы добавить для надёжности, но в этом примере мы будем без них. - -Естественно, раз в нашем распоряжении есть готовый генератор `generateSequence`, то хорошо бы его использовать. - -Конечно, можно внутри `generateAlphaNum` запустить несколько раз `generateSequence`, объединить результаты и вернуть. Так мы бы сделали с обычными функциями. Но композиция -- это кое-что получше. - -Она выглядит так: - -```js -//+ run -'use strict'; - -function* generateSequence(start, end) { - for (let i = start; i <= end; i++) yield i; -} - -function* generateAlphaNum() { - -*!* - // 0..9 - yield* generateSequence(48, 57); - - // A..Z - yield* generateSequence(65, 90); - - // a..z - yield* generateSequence(97, 122); -*/!* - -} - -let str = ''; - -for(let code of generateAlphaNum()) { - str += String.fromCharCode(code); -} - -alert(str); // 0..9A..Za..z -``` - -Здесь использована специальная форма `yield*`. Она применима только к другому генератору и *делегирует* ему выполнение. - -То есть, при `yield*` интерпретатор переходит внутрь генератора-аргумента, к примеру, `generateSequence(48, 57)`, выполняет его, и все `yield`, которые он делает, выходят из внешнего генератора. - -Получается -- как будто мы вставили код внутреннего генератора во внешний напрямую, вот так: - -```js -//+ run -'use strict'; - -function* generateSequence(start, end) { - for (let i = start; i <= end; i++) yield i; -} - -function* generateAlphaNum() { - -*!* - // yield* generateSequence(48, 57); - for (let i = 48; i <= 57; i++) yield i; - - // yield* generateSequence(65, 90); - for (let i = 65; i <= 90; i++) yield i; - - // yield* generateSequence(97, 122); - for (let i = 97; i <= 122; i++) yield i; -*/!* - -} - -let str = ''; - -for(let code of generateAlphaNum()) { - str += String.fromCharCode(code); -} - -alert(str); // 0..9A..Za..z -``` - -Код выше по поведению полностью идентичен варианту с `yield*`. При этом, конечно, переменные вложенного генератора не попадают во внешний, "делегирование" только выводит результаты `yield` во внешний поток. - -Композиция -- это естественное встраивание одного генератора в поток другого. При композиции значения из вложенного генератора выдаются "по мере готовности". Поэтому она будет работать даже если поток данных из вложенного генератора оказался бесконечным или ожидает какого-либо условия для завершения. - - -## yield -- дорога в обе стороны - -До этого генераторы наиболее напоминали "итераторы на стероидах". Но, как мы сейчас увидим, это не так, есть фундаментальное различие, генераторы гораздо мощнее и гибче. - -Всё дело в том, что `yield` -- дорога в обе стороны: он не только возвращает результат наружу, но и может передавать значение извне в генератор. - -Вызов `let result = yield value` делает следующее: - -
    -
  • Возвращает `value` во внешний код, приостанавливая выполнение генератора.
  • -
  • Внешний код может обработать значение, и затем вызвать `next` с аргументом: `generator.next(arg)`.
  • -
  • Генератор продолжит выполнение, аргумент `next` будет возвращён как результат `yield` (и записан в `result`).
  • -
- -Продемонстрируем это на примере: - -```js -//+ run -'use strict'; - -function* gen() { -*!* - // Передать вопрос во внешний код и подождать ответа - let result = yield "Сколько будет 2 + 2?"; -*/!* - - alert(result); -} - -let generator = gen(); - -let question = generator.next().value; -// "Сколько будет 2 + 2?" - -setTimeout(() => generator.next(4), 2000); -``` - -На рисунке ниже прямоугольником изображён генератор, а вокруг него -- "внешний код", который с ним взаимодействует: - - - -На этой иллюстрации показано то, что происходит в генераторе: - -
    -
  1. Первый вызов `generator.next()` -- всегда без аргумента, он начинает выполнение и возвращает результат первого `yield` ("Сколько будет 2+2?"). На этой точке генератор приостанавливает выполнение.
  2. -
  3. Результат `yield` переходит во внешний код (в `question`). Внешний код может выполнять любые асинхронные задачи, генератор стоит "на паузе".
  4. -
  5. Когда асинхронные задачи готовы, внешний код вызывает `generator.next(4)` с аргументом. Выполнение генератора возобновляется, а `4` выходит из присваивания как результат `let result = yield ...`.
  6. -
- -В примере выше -- только два `next`. - -Увеличим их количество, чтобы стал более понятен общий поток выполнения: - -```js -//+ run -'use strict'; - -function* gen() { - let ask1 = yield "Сколько будет 2 + 2?"; - - alert(ask1); // 4 - - let ask2 = yield "3 * 3?" - - alert(ask2); // 9 -} - -let generator = gen(); - -alert( generator.next().value ); // "...2+2?" - -alert( generator.next(4).value ); // "...3*3?" - -alert( generator.next(9).done ); // true -``` - -Взаимодействие с внешним кодом: - - - -
    -
  1. Первый `.next()` начинает выполнение... Оно доходит до первого `yield`.
  2. -
  3. Результат возвращается во внешний код.
  4. -
  5. Второй `.next(4)` передаёт `4` обратно в генератор как результат первого `yield` и возобновляет выполнение.
  6. -
  7. ...Оно доходит до второго `yield`, который станет результатом `.next(4)`.
  8. -
  9. Третий `next(9)` передаёт `9` в генератор как результат второго `yield` и возобновляет выполнение, которое завершается окончанием функции, так что `done: true`.
  10. -
- -Получается "пинг-понг": каждый `next(value)` передаёт в генератор значение, которое становится результатом текущего `yield`, возобновляет выполнение и получает выражение из следующего `yield`. Исключением является первый вызов `next`, который не может передать значение в генератор, т.к. ещё не было ни одного `yield`. - -На рисунке ниже изображены шаги 3-4 из примера: - - - - - -## generator.throw - -Как мы видели в примерах выше, внешний код может вернуть генератору в качестве результата `yield` любое значение. - -...Но "вернуть" можно не только результат, но и ошибку! - -Для того, чтобы передать в `yield` ошибку, используется вызов `generator.throw(err)`. При этом на строке с `yield` возникает исключение. - -Например, в коде ниже обращение к внешнему коду `yield "Сколько будет 2 + 2"` завершится с ошибкой: - - -```js -//+ run -'use strict'; - -function* gen() { - try { - // в этой строке возникнет ошибка - let result = yield "Сколько будет 2 + 2?"; // (**) - - alert("выше будет исключение ^^^"); - } catch(e) { - alert(e); // выведет ошибку - } -} - -let generator = gen(); - -let question = generator.next().value; - -*!* -generator.throw(new Error("ответ не найден в моей базе данных")); // (*) -*/!* -``` - -"Вброшенная" в строке `(*)` ошибка возникает в строке с `yield` `(**)`. Далее она обрабатывается как обычно. В примере выше она перехватывается `try..catch` и выводится. - -Если ошибку не перехватить, то она "выпадет" из генератора. По стеку ближайший вызов, который инициировал выполнение -- это строка с `.throw`. Можно перехватить её там, как и продемонстрировано в примере ниже: - - -```js -//+ run -'use strict'; - -function* gen() { - // В этой строке возникнет ошибка - let result = yield "Сколько будет 2 + 2?"; -} - -let generator = gen(); - -let question = generator.next().value; - -*!* -try { - generator.throw(new Error("ответ не найден в моей базе данных")); -} catch(e) { - alert(e); // выведет ошибку -} -*/!* -``` - -Если же ошибка и там не перехвачена, то дальше -- как обычно, либо `try..catch` снаружи, либо она "повалит" скрипт. - -## Плоский асинхронный код - -Одна из основных областей применения генераторов -- написание "плоского" асинхронного кода. - -Общий принцип такой: -
    -
  • Генератор `yield'ит` не просто значения, а промисы.
  • -
  • Есть специальная "функция-чернорабочий" `execute(generator)` которая запускает генератор, последовательными вызовами `next` получает из него промисы -- один за другим, и, когда очередной промис выполнится, возвращает его результат в генератор следующим `next`.
  • -
  • Последнее значение генератора (`done:true`) `execute` уже обрабатывает как окончательный результат -- например, возвращает через промис куда-то ещё, во внешний код или просто использует, как в примере ниже.
  • -
- -Напишем такой код для получения аватара пользователя с github и его вывода, аналогичный рассмотренному в статье про [промисы](/promise). - -Для AJAX-запросов будем использовать метод [fetch](/fetch), он как раз возвращает промисы. - -```js -//+ run -'use strict'; - -// генератор для получения и показа аватара -// он yield'ит промисы -function* showUserAvatar() { - - let userFetch = yield fetch('/article/generator/user.json'); - let userInfo = yield userFetch.json(); - - let githubFetch = yield fetch(`https://api.github.com/users/${userInfo.name}`); - let githubUserInfo = yield githubFetch.json(); - - let img = new Image(); - img.src = githubUserInfo.avatar_url; - img.className = "promise-avatar-example"; - document.body.appendChild(img); - - yield new Promise(resolve => setTimeout(resolve, 3000)); - - img.remove(); - - return img.src; -} - -// вспомогательная функция-чернорабочий -// для выполнения промисов из generator -function execute(generator, yieldValue) { - - let next = generator.next(yieldValue); - - if (!next.done) { - next.value.then( - result => execute(generator, result), - err => generator.throw(err) - ); - } else { - // обработаем результат return из генератора - // обычно здесь вызов callback или что-то в этом духе - alert(next.value); - } - -} - -*!* -execute( showUserAvatar() ); -*/!* -``` - -Функция `execute` в примере выше -- универсальная, она может работать с любым генератором, который `yield'ит` промисы. - -Вместе с тем, это -- всего лишь набросок, чтобы было понятно, как такая функция в принципе работает. Есть уже готовые реализации, обладающие большим количеством возможностей. - -Одна из самых известных -- это библиотека [co](https://github.com/tj/co), которую мы рассмотрим далее. - -## Библиотека "co" - -Библиотека `co`, как и `execute` в примере выше, получает генератор и выполняет его. - -Начнём сразу с примера, а потом -- детали и полезные возможности: - -```js -//+ run - -'use strict'; - -co(function*() { - - let result = yield new Promise( - resolve => setTimeout(resolve, 1000, 1) - ); - - alert(result); // 1 - -}) -``` - - -Предполагается, что библиотека `co` подключена к странице , например, отсюда: [](http://cdnjs.com/libraries/co/). В примере выше `function*()` делает `yield` промиса с `setTimeout`, который через секунду возвращает `1`. - -Вызов `co(…)` возвращает промис с результатом генератора. Если в примере выше `function*()` что-то возвратит, то это можно будет получить через `.then` в результате `co`: - -```js -//+ run -'use strict'; - -co(function*() { - - let result = yield new Promise( - resolve => setTimeout(resolve, 1000, 1) - ); - -*!* - return result; // return 1 - -}).then(alert); // 1 -*/!* -``` -[warn header="Обязательно нужен `catch`"] - -Частая ошибка начинающих -- вообще забывать про обработку результата `co`. Даже если результата нет, ошибки нужно обработать через `catch`, иначе они "подвиснут" в промисе. - -Такой код ничего не выведет: - -```js -//+ run -co(function*() { - throw new Error("Sorry that happened"); -}) -``` - -Программист даже не узнает об ошибке. Особенно обидно, когда это опечатка или другая программная ошибка, которую обязательно нужно поправить. - -Правильный вариант: - -```js -//+ run -co(function*() { - throw new Error("Sorry that happened"); -}).catch(alert); // обработать ошибку как-либо -``` - -Большинство примеров этого `catch` не содержат, но это лишь потому, что в примерах ошибок нет. А в реальном коде обязательно нужен `catch`. - -[/warn] - -Библиотека `co` умеет выполнять не только промисы. Есть несколько видов значений, которые можно `yield`, и их обработает `co`: - -
    -
  • Промис.
  • -
  • Объект-генератор.
  • -
  • Функция-генератор `function*()` -- `co` её выполнит, затем выполнит полученный генератор.
  • -
  • Функция с единственным аргументом вида `function(callback)` -- библиотека `co` её запустит со своей функцией-`callback` и будет ожидать, что при ошибке она вызовет `callback(err)`, а при успешном выполнении -- `callback(null, result)`. То есть, в первом аргументе -- будет ошибка (если есть), а втором -- результат (если нет ошибки). После чего результат будет передан в генератор.
  • -
  • Массив или объект из вышеперечисленного. При этом все задачи будут выполнены параллельно, и результат, в той же структуре, будет выдан наружу.
  • -
- -В примере ниже происходит `yield` всех этих видов значений. Библиотека `co` обеспечивает их выполнение и возврат результата в генератор: - -```js -//+ run -'use strict'; - -Object.defineProperty(window, 'result', { - // присвоение result=… будет выводить значение - set: value => alert(JSON.stringify(value)) -}); - -co(function*() { - result = yield function*() { // генератор - return 1; - }(); - - result = yield function*() { // функция-генератор - return 2; - }; - - result = yield Promise.resolve(3); // промис - - result = yield function(callback) { // function(callback) - setTimeout(() => callback(null, 4), 1000); - }; - - - result = yield { // две задачи выполнит параллельно, как Promise.all - one: Promise.resolve(1), - two: function*() { return 2; } - }; - - result = yield [ // две задачи выполнит параллельно, как Promise.all - Promise.resolve(1), - function*() { return 2 } - ]; - -}); -``` - -[smart header="Устаревший `yield function(callback)`"] -Отдельно заметим вариант с `yield function(callback)`. Такие функции, с единственным-аргументом callback'ом, в англоязычной литературе называют "thunk". - -Функция обязана выполниться и вызвать (асинхронно) либо `callback(err)` с ошибкой, либо `callback(null, result)` с результатом. - -Использование таких функций в `yield` является устаревшим подходом, так как там, где можно использовать `yield function(callback)`, можно использовать и промисы. При этом промисы мощнее. Но в старом коде его ещё можно встретить. -[/smart] - - -Посмотрим пример посложнее, с композицией генераторов: - -```js -//+ run -'use strict'; - -co(function*() { - let result = yield* gen(); - alert(result); // hello -}); - -function* gen() { - return yield* gen2(); -} - -function* gen2() { - let result = yield new Promise( // (1) - resolve => setTimeout(resolve, 1000, 'hello') - ); - return result; -} -``` - -Это -- отличный вариант для библиотеки `co`. Композиция `yield* gen()` вызывает `gen()` в потоке внешнего генератора. Аналогично делает и `yield* gen()`. - -Поэтому `yield new Promise` из строки `(1)` в `gen2()` попадает напрямую в библиотеку `co`, как если бы он был сделан так: - -```js -//+ run -'use strict'; - -co(function*() { - // gen() и затем gen2 встраиваются во внешний генератор - let result = yield new Promise( - resolve => setTimeout(resolve, 1000, 'hello') - ); - alert(result); // hello -}); -``` - -Пример `showUserAvatar()` можно переписать с использованием `co` вот так: - -```js -//+ run -'use strict'; - -// Загрузить данные пользователя с нашего сервера -function* fetchUser(url) { - let userFetch = yield fetch(url); - - let user = yield userFetch.json(); - - return user; -} - -// Загрузить профиль пользователя с github -function* fetchGithubUser(user) { - let githubFetch = yield fetch(`https://api.github.com/users/${user.name}`); - let githubUser = yield githubFetch.json(); - - return githubUser; -} - -// Подождать ms миллисекунд -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -// Использовать функции выше для получения аватара пользователя -function* fetchAvatar(url) { - - let user = yield* fetchUser(url); - - let githubUser = yield* fetchGithubUser(user); - - return githubUser.avatar_url; -} - -// Использовать функции выше для получения и показа аватара -function* showUserAvatar() { - - let avatarUrl; - - try { - avatarUrl = yield* fetchAvatar('/article/generator/user.json'); - } catch(e) { - avatarUrl = '/article/generator/anon.png'; - } - - let img = new Image(); - img.src = avatarUrl; - img.className = "promise-avatar-example"; - document.body.appendChild(img); - - yield sleep(2000); - - img.remove(); - - return img.src; -} - -co(showUserAvatar); -``` - -Заметим, что для перехвата ошибок при получении аватара используется `try..catch` вокруг `yield* fetchAvatar`: - -```js -try { - avatarUrl = yield* fetchAvatar('/article/generator/user.json'); -} catch(e) { - avatarUrl = '/article/generator/anon.png'; -} -``` - -Это -- одно из главных удобств использования генераторов. Несмотря на то, что операции `fetch` -- асинхронные, мы можем использовать обычный `try..catch` для обработки ошибок в них. - -## Для генераторов -- только yield* - -Библиотека `co` технически позволяет писать код так: -```js -let user = yield fetchUser(url); -// вместо -// let user = yield* fetchUser(url); -``` - -То есть, можно сделать `yield` генератора, `co()` его выполнит и передаст значение обратно. Как мы видели выше, библиотека `co` -- довольно всеядна. Однако, рекомендуется использовать для вызова функций-генераторов именно `yield*`. - -Причин для этого несколько: -
    -
  1. Делегирование генераторов `yield*` -- это встроенный механизм JavaScript. Вместо возвращения значения обратно в `co`, выполнения кода библиотеки... Мы просто используем возможности языка. Это правильнее.
  2. -
  3. Поскольку не происходит лишних вызовов, это быстрее по производительности.
  4. -
  5. И, наконец, пожалуй, самое приятное -- делегирование генераторов сохраняет стек.
  6. -
- -Проиллюстрируем последнее на примере: - -```js -//+ run -'use strict'; - -co(function*() { - -*!* - // при запуске в стеке не будет видно этой строки - yield g(); // (*) -*/!* - -}).catch(function(err) { - alert(err.stack); -}); - -function* g() { - throw new Error("my error"); -} -``` - -При запуске этого кода стек может выглядеть примерно так: -```js -*!* -at g (eval at runJS …, :13:9) -*/!* - at GeneratorFunctionPrototype.next (native) - at onFulfilled (…/co/…/index.min.js:1:1136) - at …/co/…/index.min.js:1:1076 - at co (…/co/…/index.min.js:1:1039) - at toPromise (…/co/…/index.min.js:1:1740) - at next (…/co/…/index.min.js:1:1351) - at onFulfilled (…/co/…/index.min.js:1:1172) - at …/co/…/index.min.js:1:1076 - at co (…/co/…/index.min.js:1:1039) -``` - -Детали здесь не имеют значения, самое важное -- почти весь стек находится внутри библиотеки `co`. - -Из оригинального скрипта там только одна строка (первая): -```js -at g (eval at runJS …, :13:9) -``` - -То есть, стек говорит, что ошибка возникла в строке `13`: -```js -// строка 13 из кода выше -throw new Error("my error"); -``` - -Что ж, спасибо. Но как мы оказались на этой строке? Об этом в стеке нет ни слова! - -Заменим в строке `(*)` вызов `yield` на `yield*`: - -```js -//+ run -'use strict'; - -co(function*() { - -*!* - // заменили yield на yield* - yield* g(); // (*) -*/!* - -}).catch(function(err) { - alert(err.stack); -}); - -function* g() { - throw new Error("my error"); -} -``` - -Пример стека теперь: -```js -*!* -at g (eval at runJS …, :13:9) -*/!* - at GeneratorFunctionPrototype.next (native) -*!* - at eval (eval at runJS …, :6:10) -*/!* - at GeneratorFunctionPrototype.next (native) - at onFulfilled (…/co/…/index.min.js:1:1136) - at …/co/…/index.min.js:1:1076 - at co (…/co/…/index.min.js:1:1039) -*!* - at eval (eval at runJS …, :3:1) -*/!* - at eval (native) - at runJS (…) -``` - -Если очистить от вспомогательных вызовов, то эти строки -- как раз то, что нам надо: -```js -at g (eval at runJS …, :13:9) - at eval (eval at runJS …, :6:10) - at eval (eval at runJS …, :3:1) -``` - -Теперь видно, что (читаем снизу) исходный вызов был в строке `3`, далее -- вложенный в строке `6`, и затем произошла ошибка была в строке `13`. - -Почему вариант с простым `yield` не работает -- достаточно очевидно, если внимательно посмотреть на код и воспроизвести в уме, как он работает. Оставляем это упражнение вдумчивому читателю. - -Итого, рекомендация уже достаточно обоснована -- при запуске вложенных генераторов используем `yield*`. - -## Итого - -
    -
  • Генераторы создаются при помощи функций-генераторов `function*(…) {…}`.
  • -
  • Внутри генераторов и только них разрешён оператор `yield`. Это иногда создаёт недобства, поскольку в коллбэках `.map/.forEach` сделать `yield` нельзя. Впрочем, можно сделать `yield` массива (при использовании `co`).
  • -
  • Внешний код и генератор обмениваются промежуточными результатами посредством вызовов `next/yield`.
  • -
  • Генераторы позволяют писать плоский асинхронный код, при помощи библиотки `co`.
  • -
- -Что касается кросс-браузерной поддержки -- она стремительно приближается. Пока же можно использовать генераторы вместе с [Babel](https://babeljs.io). - -[head] - - -[/head] - - - - - - - diff --git a/1-js/10-es-modern/12-generator/genYield2-2.png b/1-js/10-es-modern/12-generator/genYield2-2.png deleted file mode 100644 index 67484418..00000000 Binary files a/1-js/10-es-modern/12-generator/genYield2-2.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/genYield2-2@2x.png b/1-js/10-es-modern/12-generator/genYield2-2@2x.png deleted file mode 100644 index d6bcf78e..00000000 Binary files a/1-js/10-es-modern/12-generator/genYield2-2@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/genYield2-3.png b/1-js/10-es-modern/12-generator/genYield2-3.png deleted file mode 100644 index ccc92309..00000000 Binary files a/1-js/10-es-modern/12-generator/genYield2-3.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/genYield2-3@2x.png b/1-js/10-es-modern/12-generator/genYield2-3@2x.png deleted file mode 100644 index e4064bb0..00000000 Binary files a/1-js/10-es-modern/12-generator/genYield2-3@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/genYield2.png b/1-js/10-es-modern/12-generator/genYield2.png deleted file mode 100644 index dd29c353..00000000 Binary files a/1-js/10-es-modern/12-generator/genYield2.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/genYield2@2x.png b/1-js/10-es-modern/12-generator/genYield2@2x.png deleted file mode 100644 index 3600cb13..00000000 Binary files a/1-js/10-es-modern/12-generator/genYield2@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/generateSequence-1.png b/1-js/10-es-modern/12-generator/generateSequence-1.png deleted file mode 100644 index e5182d51..00000000 Binary files a/1-js/10-es-modern/12-generator/generateSequence-1.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/generateSequence-1@2x.png b/1-js/10-es-modern/12-generator/generateSequence-1@2x.png deleted file mode 100644 index 311a4f69..00000000 Binary files a/1-js/10-es-modern/12-generator/generateSequence-1@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/generateSequence-2.png b/1-js/10-es-modern/12-generator/generateSequence-2.png deleted file mode 100644 index 622f03a6..00000000 Binary files a/1-js/10-es-modern/12-generator/generateSequence-2.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/generateSequence-2@2x.png b/1-js/10-es-modern/12-generator/generateSequence-2@2x.png deleted file mode 100644 index e761223e..00000000 Binary files a/1-js/10-es-modern/12-generator/generateSequence-2@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/generateSequence-3.png b/1-js/10-es-modern/12-generator/generateSequence-3.png deleted file mode 100644 index 81ebadea..00000000 Binary files a/1-js/10-es-modern/12-generator/generateSequence-3.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/generateSequence-3@2x.png b/1-js/10-es-modern/12-generator/generateSequence-3@2x.png deleted file mode 100644 index 440fd364..00000000 Binary files a/1-js/10-es-modern/12-generator/generateSequence-3@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/generateSequence-4.png b/1-js/10-es-modern/12-generator/generateSequence-4.png deleted file mode 100644 index a9c7d7a5..00000000 Binary files a/1-js/10-es-modern/12-generator/generateSequence-4.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/generateSequence-4@2x.png b/1-js/10-es-modern/12-generator/generateSequence-4@2x.png deleted file mode 100644 index f76061df..00000000 Binary files a/1-js/10-es-modern/12-generator/generateSequence-4@2x.png and /dev/null differ diff --git a/1-js/10-es-modern/12-generator/user.json b/1-js/10-es-modern/12-generator/user.json deleted file mode 100644 index 32f89971..00000000 --- a/1-js/10-es-modern/12-generator/user.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "iliakan", - "isAdmin": true -} diff --git a/1-js/10-es-modern/2-let-const/article.md b/1-js/10-es-modern/2-let-const/article.md deleted file mode 100644 index a3ebaf30..00000000 --- a/1-js/10-es-modern/2-let-const/article.md +++ /dev/null @@ -1,209 +0,0 @@ -# Переменные: let и const - -В ES-2015 предусмотрены новые способы объявления переменных: через `let` и `const` вместо `var`. - -Например: -```js -let a = 5; -``` - -## let - -У объявлений переменной через `let` есть три основных отличия от `var`: - -
    -
  1. **Область видимости переменной `let` -- блок `{...}`.** - -Как мы помним, переменная, объявленная через `var`, видна везде в функции. - -Переменная, объявленная через `let`, видна только в рамках блока `{...}`, в котором объявлена. - -Это, в частности, влияет на объявления внутри `if`, `while` или `for`. - -Например, переменная через `var`: - -```js -//+ run -'use strict'; - -var apples = 5; - -if (true) { - var apples = 10; - - alert(apples); // 10 (внутри блока) -} - -alert(apples); // 10 (снаружи блока то же самое) -``` - -В примере выше `apples` -- одна переменная на весь код, которая модифицируется в `if`. - -То же самое с `let` будет работать по-другому: - -```js -//+ run -'use strict'; - -let apples = 5; // (*) - -if (true) { - let apples = 10; - - alert(apples); // 10 (внутри блока) -} - -*!* -alert(apples); // 5 (снаружи блока значение не изменилось) -*/!* -``` - -Здесь, фактически, две независимые переменные `apples`, одна -- глобальная, вторая -- в блоке `if`. - -Заметим, что если объявление `let apples` в первой строке `(*)` удалить, то в последнем `alert` будет ошибка: переменная неопределена: - -```js -//+ run -'use strict'; - -if (true) { - let apples = 10; - - alert(apples); // 10 (внутри блока) -} - -*!* -alert(apples); // ошибка! -*/!* -``` - - -Это потому что переменная `let` всегда видна именно в том блоке, где объявлена, и не более. - -
  2. -
  3. **Переменная `let` видна только после объявления.** - -Как мы помним, переменные `var` существуют и до объявления. Они равны `undefined`: - -```js -//+ run -'use strict'; - -alert(a); // undefined - -var a = 5; -``` - -С переменными `let` всё проще. До объявления их вообще нет. - -Такой доступ приведёт к ошибке: -```js -//+ run -'use strict'; - -*!* -alert(a); // ошибка, нет такой переменной -*/!* - -let a = 5; -``` - -Заметим также, что переменные `let` нельзя повторно объявлять. То есть, такой код выведет ошибку: - -```js -//+ run -'use strict'; - -let x; -let x; // ошибка: переменная x уже объявлена -``` - -Это -- хоть и выглядит ограничением по сравнению с `var`, но на самом деле проблем не создаёт. Например, два таких цикла совсем не конфликтуют: -```js -//+ run -'use strict'; - -// каждый цикл имеет свою переменную i -for(let i = 0; i<10; i++) { /* … */ } -for(let i = 0; i<10; i++) { /* … */ } - -alert( i ); // ошибка: глобальной i нет -``` - -При объявлении внутри цикла переменная `i` будет видна только в блоке цикла. Она не видна снаружи, поэтому будет ошибка в последнем `alert`. - - -
  4. -
  5. **При использовании в цикле, для каждой итерации создаётся своя переменная.** - -Переменная `var` -- одна на все итерации цикла и видна даже после цикла: - -```js -//+ run -for(var i=0; i<10; i++) { /* … */ } - -alert(i); // 10 -``` - -С переменной `let` -- всё по-другому. - -Каждому повторению цикла соответствует своя независимая переменная `let`. Если внутри цикла есть вложенные объявления функций, то в замыкании каждой будет та переменная, которая была при соответствующей итерации. - -Это позволяет легко решить классическую проблему с замыканиями, описанную в задаче [](/task/make-army). - -```js -//+ run -'use strict'; - -function makeArmy() { - - let shooters = []; - - for (*!*let*/!* i = 0; i < 10; i++) { - shooters.push(function() { - alert( i ); // выводит свой номер - }); - } - - return shooters; -} - -var army = makeArmy(); - -army[0](); // 0 -army[5](); // 5 -``` - -Если бы объявление было `var i`, то была бы одна переменная `i` на всю функцию, и вызовы в последних строках выводили бы `10` (подробнее -- см. задачу [](/task/make-army)). - -А выше объявление `let i` создаёт для каждого повторения блока в цикле свою переменную, которую функция и получает из замыкания в последних строках. -
  6. -
- -## const - -Объявление `const` задаёт константу, то есть переменную, которую нельзя менять: - -```js -//+ run -'use strict'; - -const apple = 5; -apple = 10; // ошибка -``` - -В остальном объявление `const` полностью аналогично `let`. - -## Итого - - -Переменные `let`: - -
    -
  • Видны только после объявления и только в текущем блоке.
  • -
  • Нельзя переобъявлять (в том же блоке).
  • -
  • При объявлении переменной в цикле `for(let …)` -- она видна только в этом цикле. Причём каждой итерации соответствует своя переменная `let`.
  • -
- -Переменная `const` -- это константа, в остальном -- как `let`. - diff --git a/1-js/10-es-modern/3-destructuring/article.md b/1-js/10-es-modern/3-destructuring/article.md deleted file mode 100644 index cca7483f..00000000 --- a/1-js/10-es-modern/3-destructuring/article.md +++ /dev/null @@ -1,324 +0,0 @@ - -# Деструктуризация - -*Деструктуризация* (destructuring assignment) -- это особый синтаксис присваивания, при котором можно присвоить массив или объект сразу нескольким переменным, разбив его на части. - -## Массив - -Пример деструктуризации массива: - -```js -'use strict'; - -let [firstName, lastName] = ["Илья", "Кантор"]; - -alert(firstName); // Илья -alert(lastName); // Кантор -``` - -При таком присвоении первое значение массива пойдёт в переменную `firstName`, второе -- в `lastName`, а последующие (если есть) -- будут отброшены. - -Ненужные элементы массива также можно отбросить, поставив лишнюю запятую: - -```js -//+ run -'use strict'; - -*!* -// первый и второй элементы не нужны -let [, , title] = "Юлий Цезарь Император Рима".split(" "); -*/!* - -alert(title); // Император -``` - -В коде выше первый и второй элементы массива никуда не записались, они были отброшены. Как, впрочем, и все элементы после третьего. - -### Оператор "spread" - -Если мы хотим получить и последующие значения массива, но не уверены в их числе -- можно добавить ещё один параметр, который получит "всё остальное", при помощи оператора `"..."` ("spread", троеточие): - -```js -//+ run -'use strict'; - -*!* -let [firstName, lastName, ...rest] = "Юлий Цезарь Император Рима".split(" "); -*/!* - -alert(firstName); // Юлий -alert(lastName); // Цезарь -alert(rest); // Император,Рима (массив из 2х элементов) -``` - -Значением `rest` будет массив из оставшихся элементов массива. Вместо `rest` можно использовать и другое имя переменной, оператор здесь -- троеточие. Оно должно стоять только последним элементом в списке слева. - -### Значения по умолчанию - -Если значений в массиве меньше, чем переменных -- ошибки не будет, просто присвоится `undefined`: - - -```js -//+ run -'use strict'; - -*!* -let [firstName, lastName] = []; -*/!* - -alert(firstName); // undefined -``` - -Впрочем, как правило, в таких случаях задают значение по умолчанию. Для этого нужно после переменной использовать символ `=` со значением, например: - -```js -//+ run -'use strict'; - -*!* -// значения по умолчанию -let [firstName="Гость", lastName="Анонимный"] = []; -*/!* - -alert(firstName); // Гость -alert(lastName); // Анонимный -``` - -В качестве значений по умолчанию можно использовать не только примитивы, но и выражения, даже включающие в себя вызовы функций: - -```js -//+ run -'use strict'; - -function defaultLastName() { - return Date.now() + '-visitor'; -} - -*!* -// lastName получит значение, соответствующее текущей дате: -let [firstName, lastName=defaultLastName()] = ["Вася"]; -*/!* - -alert(firstName); // Вася -alert(lastName); // 1436...-visitor -``` - -Заметим, что вызов функции `defaultLastName()` для генерации значения по умолчанию будет осуществлён только при необходимости, то есть если значения нет в массиве. - -## Деструктуризация объекта - -Деструктуризацию можно использовать и с объектами. При этом мы указываем, какие свойства в какие переменные должны "идти". - -Базовый синтаксис: -```js -let {var1, var2} = {var1:…, var2…} -``` - -Объект справа -- уже существующий, который мы хотим разбить на переменные. А слева -- список переменных, в которые нужно соответствующие свойства записать. - -Например: - -```js -//+ run -'use strict'; - -let options = { - title: "Меню", - width: 100, - height: 200 -}; - -*!* -let {title, width, height} = options; -*/!* - -alert(title); // Меню -alert(width); // 100 -alert(height); // 200 -``` - -Как видно, свойства `options.title`, `options.width` и `options.height` автоматически присвоились соответствующим переменным. - -Если хочется присвоить свойство объекта в переменную с другим именем, например, чтобы свойство `options.width` пошло в переменную `w`, то можно указать соответствие через двоеточие, вот так: - -```js -//+ run -'use strict'; - -let options = { - title: "Меню", - width: 100, - height: 200 -}; - -*!* -let {width: w, height: h, title} = options; -*/!* - -alert(title); // Меню -alert(w); // 100 -alert(h); // 200 -``` - -В примере выше свойство `width` отправилось в переменную `w`, свойство `height` -- в переменную `h`, а `title` -- в переменную с тем же названием. - -Если каких-то свойств в объекте нет, можно указать значение по умолчанию через знак равенства `=`, вот так; - -```js -//+ run -'use strict'; - -let options = { - title: "Меню" -}; - -*!* -let {width=100, height=200, title} = options; -*/!* - -alert(title); // Меню -alert(width); // 100 -alert(height); // 200 -``` - -Можно и сочетать одновременно двоеточие и равенство: - - -```js -//+ run -'use strict'; - -let options = { - title: "Меню" -}; - -*!* -let {width:w=100, height:h=200, title} = options; -*/!* - -alert(title); // Меню -alert(w); // 100 -alert(h); // 200 -``` - -А что, если в объекте больше значений, чем переменных? Можно ли куда-то присвоить "остаток", аналогично массивам? - -Такой возможности в текущем стандарте нет. Она планируется в будущем стандарте, и выглядеть она будет примерно так: - -```js -//+ run -'use strict'; - -let options = { - title: "Меню", - width: 100, - height: 200 -}; - -*!* -let {title, ...size} = options; -*/!* - -// title = "Меню" -// size = { width: 100, height: 200} (остаток) -``` - -Этот код будет работать, например, при использовании Babel со включёнными экспериментальными возможностями, но ещё раз заметим, что в текущий стандарт такая возможность не вошла. - -[smart header="Деструктуризация без объявления"] - -В примерах выше переменные объявлялись прямо перед присваиванием: `let {…} = {…}`. Конечно, можно и без `let`, использовать уже существующие переменные. - -Однако, здесь есть небольшой "подвох". В JavaScript, если в основном потоке кода (не внутри другого выражения) встречается `{...}`, то это воспринимается как блок. - -Например, можно использовать такой блок для ограничения видимости переменных: -```js -//+ run -'use strict'; -{ - // вспомогательные переменные, локальные для блока - let a = 5; - // поработали с ними - alert(a); // 5 - // больше эти переменные не нужны -} -alert(a); // ошибка нет такой переменной -``` - -Конечно, это бывает удобно, но в данном случае это создаст проблему при деструктуризации: - -```js -let a, b; -{a, b} = {a:5, b:6}; // будет ошибка, оно посчитает, что {a,b} - блок -``` - -Чтобы избежать интерпретации `{a, b}` как блока, нужно обернуть всё присваивание в скобки: - -```js -let a, b; -({a, b} = {a:5, b:6}); // внутри выражения это уже не блок -``` -[/smart] - -## Вложенные деструктуризации - -Если объект или массив содержат другие объекты или массивы, и их тоже хочется разбить на переменные -- не проблема. - -Деструктуризации можно как угодно сочетать и вкладывать друг в друга. - -В коде ниже `options` содержит подобъект и подмассив. В деструктуризации ниже сохраняется та же структура: - -```js -//+ run -'use strict'; - -let options = { - size: { - width: 100, - height: 200 - }, - items: ["Пончик", "Пирожное"] -} - -let { title="Меню", size: {width, height}, items: [item1, item2] } = options; - -// Меню 100 200 Пончик Пирожное -alert(title); // Меню -alert(width); // 100 -alert(height); // 200 -alert(item1); // Пончик -alert(item2); // Пирожное -``` - -Как видно, весь объект `options` корректно разбит на переменные. - - -## Итого - -
    -
  • Деструктуризация позволяет разбивать объект или массив на переменные при присвоении.
  • -
  • Синтаксис: -```js -let {prop : varName = default, ...} = object -``` - -Здесь двоеточие `:` задаёт отображение свойства `prop` в переменную `varName`, а равенство `=default` задаёт выражение, которое будет использовано, если значение отсутствует (не указано или `undefined`). - -Для массивов имеет значение порядок, поэтому нельзя использовать `:`, но значение по умолчанию -- можно: - -```js -let [var1 = default, var2, ...rest] = array -``` - -Объявление переменной в начале конструкции не обязательно. Можно использовать и существующие переменные. Однако при деструктуризации объекта может потребоваться обернуть выражение в скобки. -
  • -
  • Вложенные объекты и массивы тоже работают, при деструктуризации нужно лишь сохранить ту же структуру, что и исходный объект/массив.
  • -
- -Как мы увидим далее, деструктуризации особенно пригодятся удобны при чтении объектных параметров функций. - - - - - - diff --git a/1-js/10-es-modern/4-es-function/article.md b/1-js/10-es-modern/4-es-function/article.md deleted file mode 100644 index ab0d40e3..00000000 --- a/1-js/10-es-modern/4-es-function/article.md +++ /dev/null @@ -1,469 +0,0 @@ - -# Функции - -В функциях основные изменения касаются передачи параметров, плюс введена дополнительная короткая запись через стрелочку `=>`. - -## Параметры по умолчанию - -Можно указывать параметры по умолчанию через равенство `=`, например: - -```js -//+ run -function showMenu(title = "Без заголовка", width = 100, height = 200) { - alert(`${title} ${width} ${height}`); -} - -showMenu("Меню"); // Меню 100 200 -``` - -Параметр по умолчанию используется при отсутствующем аргументе или равном `undefined`, например: - -```js -//+ run -function showMenu(title = "Заголовок", width = 100, height = 200) { - alert(`title=${title} width=${width} height=${height}`); -} - -// По умолчанию будут взяты 1 и 3 параметры -// title=Заголовок width=null height=200 -showMenu(undefined, null); -``` - -При передаче любого значения, кроме `undefined`, включая пустую строку, ноль или `null`, параметр считается переданным, и значение по умолчание не используется. - -**Параметры по умолчанию могут быть не только значениями, но и выражениями.** - -Например: - -```js -//+ run -function sayHi(who = getCurrentUser().toUpperCase()) { - alert(`Привет, ${who}!`); -} - -function getCurrentUser() { - return 'Вася'; -} - -sayHi(); // Привет, ВАСЯ! -``` - -Заметим, что значение выражения `getCurrentUser().toUpperCase()` будет вычислено, и соответствующие функции вызваны -- лишь в том случае, если это необходимо, то есть когда функция вызвана без параметра. - -В частности, выражение по умолчанию не вычисляется при объявлении функции. В примере выше функция `getCurrentUser()` будет вызвана именно в последней строке, так как не передан параметр. - - -## Оператор spread вместо arguments - -Чтобы получить массив аргументов, можно использовать оператор `…`, например: - -```js -//+ run - -function showName(firstName, lastName, *!*...rest*/!*) { - alert(`${firstName} ${lastName} - ${rest}`); -} - -// выведет: Юлий Цезарь - Император,Рима -showName("Юлий", "Цезарь", "Император", "Рима"); -``` - -В `rest` попадёт массив всех аргументов, начиная со второго. - -Заметим, что `rest` -- настоящий массив, с методами `map`, `forEach` и другими, в отличие от `arguments`. - -[warn header="Оператор … должен быть в конце"] - -Оператор `…` собирает "все оставшиеся" аргументы, поэтому такое объявление не имеет смысла: -```js -function f(arg1, ...rest, arg2) { // arg2 после ...rest ?! - // будет ошибка -} -``` -Параметр `...rest` должен быть в конце функции. -[/warn] - - -Выше мы увидели использование `...` для чтения параметров в объявлении функции. Но этот же оператор можно использовать и при вызове функции, для передачи массива параметров как списка, например: - -```js -//+ run -'use strict'; - -let numbers = [2, 3, 15]; - -// Оператор ... в вызове передаст массив как список аргументов -// Этот вызов аналогичен Math.max(2, 3, 15) -let max = Math.max(*!*...numbers*/!*); - -alert( max ); // 15 -``` - -Формально говоря, эти два вызова делают одно и то же: - -```js -Math.max(...numbers); -Math.max.apply(Math, numbers); -``` - -Похоже, что первый -- короче и красивее. - -## Деструктуризация в параметрах - -Если функция получает объект, то она может его тут же разбить в переменные: - -```js -//+ run -'use strict'; - -let options = { - title: "Меню", - width: 100, - height: 200 -}; - -*!* -function showMenu({title, width, height}) { -*/!* - alert(`${title} ${width} ${height}`); // Меню 100 200 -} - -showMenu(options); -``` - -Можно использовать и более сложную деструктуризацию, с соответствиями и значениями по умолчанию: - -```js -//+ run -'use strict'; - -let options = { - title: "Меню" -}; - -*!* -function showMenu({title="Заголовок", width:w=100, height:h=200}) { -*/!* - alert(`${title} ${w} ${h}`); -} - -// объект options будет разбит на переменные -showMenu(options); // Меню 100 200 -``` - -Заметим, что в примере выше какой-то аргумент у `showMenu()` обязательно должен быть, чтобы разбить его на переменные. - -Если хочется, чтобы функция могла быть вызвана вообще без аргументов -- нужно добавить ей параметр по умолчанию -- уже не внутрь деструктуризации, а в самом списке аргументов: - -```js -//+ run -'use strict'; - -function showMenu({title="Заголовок", width:w=100, height:h=200} *!*= {}*/!*) { - alert(`${title} ${w} ${h}`); -} - -showMenu(); // Заголовок 100 200 -``` - -В коде выше весь объект аргументов по умолчанию равен пустому объекту `{}`, поэтому всегда есть что деструктуризовать. - -## Имя "name" - -В свойстве `name` у функции находится её имя. - -Например: - -```js -//+ run -'use strict'; - -function f() {} // f.name == "f" - -let g = function g() {}; // g.name == "g" - -alert(`${f.name} ${g.name}`) // f g -``` - -В примере выше показаны Function Declaration и Named Function Expression. В синтаксисе выше довольно очевидно, что у этих функций есть имя `name`. В конце концов, оно указано в объявлении. - -Но современный JavaScript идёт дальше, он старается даже анонимным функциям дать разумные имена. - -Например, при создании анонимной функции с одновременной записью в переменную или свойство -- её имя равно названию переменной (или свойства). - -Например: - -```js -'use strict'; - -// свойство g.name = "g" -let g = function() {}; - -let user = { - // свойство user.sayHi.name == "sayHi" - sayHi: function() { }; -} -``` - -## Функции в блоке - -Объявление функции Function Declaration, сделанное в блоке, видно только в этом блоке. - -Например: - -```js -//+ run -'use strict'; - -if (true) { - - sayHi(); // работает - - function sayHi() { - alert("Привет!"); - } - -} -sayHi(); // ошибка, функции не существует -``` - -То есть, иными словами, такое объявление -- ведёт себя в точности как если бы `let sayHi = function() {…}` было сделано в начале блока. - -## Функции через => - -Появился новый синтаксис для задания функций через "стрелку" `=>`. - -Его простейший вариант выглядит так: -```js -//+ run -'use strict'; - -*!* -let inc = x => x+1; -*/!* - -alert( inc(1) ); // 2 -``` - -Эти две записи -- примерно аналогичны: - -```js -let inc = x => x+1; - -let inc = function(x) { return x + 1; }; -``` - -Как видно, `"x => x+1"` -- это уже готовая функция. Слева от `=>` находится аргумент, а справа -- выражение, которое нужно вернуть. - -Если аргументов несколько, то нужно обернуть их в скобки, вот так: - -```js -//+ run -'use strict'; - -*!* -let sum = (a,b) => a + b; -*/!* - -// аналог с function -// let inc = function(a, b) { return a + b; }; - -alert( sum(1, 2) ); // 3 -``` - -Если нужно задать функцию без аргументов, то также используются скобки, в этом случае -- пустые: - -```js -//+ run -'use strict'; - -*!* -// вызов getTime() будет возвращать текущее время -let getTime = () => `${new Date().getHours()} : ${new Date().getMinutes()}`; -*/!* - -alert( getTime() ); // текущее время -``` - -Когда тело функции достаточно большое, то можно его обернуть в фигурные скобки `{…}`: - - -```js -//+ run -'use strict'; - -*!* -let getTime = () => { - let date = new Date(); - let hours = date.getHours(); - let minutes = date.getMinutes(); - return `${hours}:${minutes}`; -}; -*/!* - -alert( getTime() ); // текущее время -``` - -Заметим, что как только тело функции оборачивается в `{…}`, то её результат уже не возвращается автоматически. Такая функция должна делать явный `return`, как в примере выше, если конечно хочет что-либо возвратить. - -Функции-стрелки очень удобны в качестве коллбеков, например: - -```js -//+ run -`use strict`; - -let arr = [5, 8, 3]; - -*!* -let sorted = arr.sort( (a,b) => a - b ); -*/!* - -alert(sorted); // 3, 5, 8 -``` - -Такая запись -- коротка и понятна. Далее мы познакомимся с дополнительными преимуществами использования функций-стрелок для этой цели. - -## Функции-стрелки не имеют своего this - -Внутри функций-стрелок -- тот же `this`, что и снаружи. - -Это очень удобно в обработчиках событий и коллбэках, например: - -```js -//+ run -'use strict'; - -let group = { - title: "Наш курс", - students: ["Вася", "Петя", "Даша"], - - showList: function() { -*!* - this.students.forEach( - (student) => alert(`${this.title}: ${student}`) - ) -*/!* - } -} - -group.showList(); -// Наш курс: Вася -// Наш курс: Петя -// Наш курс: Даша -``` - -Здесь в `forEach` была использована функция-стрелка, поэтому `this.title` в коллбэке -- тот же, что и во внешней функции `showList`. То есть, в данном случае -- `group.title`. - -Если бы в `forEach` вместо функции-стрелки была обычная функция, то была бы ошибка: - -```js -//+ run -'use strict'; - -let group = { - title: "Наш курс", - students: ["Вася", "Петя", "Даша"], - - showList: function() { -*!* - this.students.forEach(function(student) { - alert(`${this.title}: ${student}`); // будет ошибка - }) -*/!* - } -} - -group.showList(); -``` - -При запуске будет "попытка прочитать свойство `title` у `undefined`", так как `.forEach(f)` при запуске `f` не ставит `this`. То есть, `this` внутри `forEach` будет `undefined`. - -[warn header="Функции стрелки нельзя запускать с `new`"] -Отсутствие у функции-стрелки "своего `this`" влечёт за собой естественное ограничение: такие функции нельзя использовать в качестве конструктора, то есть нельзя вызывать через `new`. -[/warn] - -## Функции-стрелки не имеют своего arguments - -В качестве `arguments` используются аргументы внешней "обычной" функции. - -Например: - -```js -//+ run -'use strict'; - -function f() { - let showArg = () => alert(arguments[0]); - showArg(); -} - -f(1); // 1 -``` - -Вызов `showArg()` выведет `1`, получив его из аргументов функции `f`. Функция-стрелка здесь вызвана без параметров, но это не важно: `arguments` всегда берутся из внешней "обычной" функции. - -Сохранение внешнего `this` и `arguments` удобно использовать для форвардинга вызовов и создания декораторов. - -Например, декоратор `defer(f, ms)` ниже получает функцию `f` и возвращает обёртку вокруг неё, откладывающую вызов на `ms` миллисекунд: - -```js -//+ run -'use strict'; - -*!* -function defer(f, ms) { - return function() { - setTimeout(() => f.apply(this, arguments), ms) - } -} -*/!* - -function sayHi(who) { - alert(`Привет, ${who}!`); -} - -let sayHiDeferred = defer(sayHi, 2000); -sayHiDeferred("Вася"); // Привет, Вася! через 2 секунды -``` - -Аналогичная реализация без функции-стрелки выглядела бы так: - -```js -function defer(f, ms) { - return function() { -*!* - let args = arguments; - let ctx = this; -*/!* - setTimeout(function() { - return f.apply(ctx, args); - }, ms); - } -} -``` - -В этом коде пришлось создавать дополнительные переменные `args` и `ctx` для передачи внешних аргументов и контекста через замыкание. - - -## Итого - -Основные улучшения в функциях: -
    -
  • Можно задавать параметры по умолчанию, а также использовать деструктуризацию для чтения приходящего объекта.
  • -
  • Оператор spread (троеточие) в объявлении позволяет функции получать оставшиеся аргументы в массив: `function f(arg1, arg2, ...rest)`.
  • -
  • Тот же оператор spread в вызове функции позволяет передать её массив как список аргументов (вместо `apply`).
  • -
  • У функции есть свойство `name`, оно содержит имя, указанное при объявлении функции, либо, если его нет, то имя свойства или переменную, в которую она записана. Есть и некоторые другие ситуации, в которых интерпретатор подставляет "самое подходящее" имя.
  • -
  • Объявление Function Declaration в блоке `{...}` видно только в этом блоке.
  • -
  • Появились функции-стрелки: -
      -
    • Без фигурных скобок возвращают выражение `expr`: `(args) => expr`.
    • -
    • С фигурными скобками требуют явного `return`.
    • -
    • Сохраняют `this` и `arguments` окружающего контекста.
    • -
    • Не могут быть использованы как конструкторы, с `new`.
    • -
    -
  • -
- - - - diff --git a/1-js/10-es-modern/5-es-string/article.md b/1-js/10-es-modern/5-es-string/article.md deleted file mode 100644 index f28c4b8a..00000000 --- a/1-js/10-es-modern/5-es-string/article.md +++ /dev/null @@ -1,332 +0,0 @@ - -# Строки - -Есть ряд улучшений и новых методов для строк. - -Начнём с, пожалуй, самого важного. - -## Строки-шаблоны - -Добавлен новый вид кавычек для строк: -```js -let str = `обратные кавычки`; -``` - -Основные отличия от двойных `"…"` и одинарных `'…'` кавычек: - -
    -
  • **В них разрешён перевод строки.** - -Например: -```js -//+ run -alert(`моя - многострочная - строка`); -``` -Заметим, что пробелы и, собственно, перевод строки также входят в строку, и будут выведены. -
  • -
  • **Можно вставлять выражения при помощи `${…}`.** - -Например: -```js -//+ run -'use strict'; -let apples = 2; -let oranges = 3; - -alert(`${apples} + ${oranges} = ${apples + oranges}`); // 2 + 3 = 5 -``` - -Как видно, при помощи `${…}` можно вставлять как и значение переменной `${apples}`, так и более сложные выражения, которые могут включать в себя операторы, вызовы функций и т.п. Такую вставку называют "интерполяцией". -
  • -
- -## Функции шаблонизации - -Можно использовать свою функцию шаблонизации для строк. - -Название этой функции ставится перед первой обратной кавычкой: -```js -let str = func`моя строка`; -``` - -Эта функция будет автоматически вызвана и получит в качестве аргументов строку, разбитую по вхождениям параметров `${…}` и сами эти параметры. - -Например: - -```js -//+ run -'use strict'; - -function f(strings, ...values) { - alert(JSON.stringify(strings)); // ["Sum of "," + "," =\n ","!"] - alert(JSON.stringify(strings.raw)); // ["Sum of "," + "," =\\n ","!"] - alert(JSON.stringify(values)); // [3,5,8] -} - -let apples = 3; -let oranges = 5; - -// | s[0] | v[0] |s[1]| v[1] |s[2] | v[2] |s[3] -let str = f`Sum of ${apples} + ${oranges} =\n ${apples + oranges}!`; -``` - -В примере выше видно, что строка разбивается по очереди на части: "кусок строки" -- "параметр" -- "кусок строки" -- "параметр". - -
    -
  • Участки строки идут в первый аргумент-массив `strings`.
  • -
  • У этого массива есть дополнительное свойство `strings.raw`. В нём находятся строки в точности как в оригинале. Это влияет на спец-символы, например в `strings` символ `\n` -- это перевод строки, а в `strings.raw` -- это именно два символа `\n`.
  • -
  • Дальнейший список аргументов функции шаблонизации -- это значения выражений в `${...}`, в данном случае их три.
  • -
- -[smart header="Зачем `strings.raw`?"] -В отличие от `strings`, в `strings.raw` содержатся участки строки в "изначально введённом" виде. - -То есть, если в строке находится `\n` или `\u1234` или другое особое сочетание символов, то оно таким и останется. - -Это нужно в тех случаях, когда функция шаблонизации хочет произвести обработку полностью самостоятельно (свои спец. символы?). Или же когда обработка спец. символов не нужна -- например, строка содержит "обычный текст", набранный непрограммистом без учёта спец. символов. -[/smart] - -Как видно, функция имеет доступ ко всему: к выражениям, к участкам текста и даже, через `strings.raw` -- к оригинально введённому тексту без учёта стандартных спец. символов. - -Функция шаблонизации может как-то преобразовать строку и вернуть новый результат. - -В простейшем случае можно просто "склеить" полученные фрагменты в строку: - -```js -//+ run -'use strict'; - -// str восстанавливает строку -function str(strings, ...values) { - let str = ""; - for(let i=0; i values[num]); -} - -// Пример использования -*!* -let name = "Вася"; - -// Перевести строку -alert( i18n`Hello, ${name}!` ); // Привет, Вася! -*/!* -``` - -Итоговое использование выглядит довольно красиво, не правда ли? - -Разумеется, эту функцию можно улучшить и расширить. Функция шаблонизации -- это своего рода "стандартный синтаксический сахар" для упрощения форматирования и парсинга строк. - -## Улучшена поддержка юникода - -Внутренняя кодировка строк в JavaScript -- это UTF-16, то есть под каждый символ отводится ровно два байта. - -Но под всевозможные символы всех языков мира 2 байт не хватает. Поэтому бывает так, что одному символу языка соответствует два юникодных символа (итого 4 байта). Такое сочетание называют "суррогатной парой". - -Самый частый пример суррогатной пары, который можно встретить в литературе -- это китайские иероглифы. - -Заметим, однако, что не всякий китайский иероглиф -- суррогатная пара. Существенная часть "основного" юникод-диапазона как раз отдана под китайский язык, поэтому некоторые иероглифы -- которые в неё "влезли" -- представляются одним юникод-символом, а те, которые не поместились (реже используемые) -- двумя. - -Например: - -```js -//+ run -alert( '我'.length ); // 1 -alert( '𩷶'.length ); // 2 -``` - -В тексте выше для первого иероглифа есть отдельный юникод-символ, и поэтому длина строки `1`, а для второго используется суррогатная пара. Соответственно, длина -- `2`. - -Китайскими иероглифами суррогатные пары, естественно, не ограничиваются. - -Ими представлены редкие математические символы, а также некоторые символы для эмоций, к примеру: - -```js -//+ run -alert( '𝒳'.length ); // 2, MATHEMATICAL SCRIPT CAPITAL X -alert( '😂'.length ); // 2, FACE WITH TEARS OF JOY -``` - -В современный JavaScript добавлены методы [String.fromCodePoint](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint) и [str.codePointAt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt) -- аналоги `String.fromCharCode` и `str.charCodeAt`, корректно работающие с суррогатными парами. - -Например, `charCodeAt` считает суррогатную пару двумя разными символами и возвращает код каждой: - -```js -//+ run -// как будто в строке два разных символа (на самом деле один) -alert( '𝒳'.charCodeAt(0) + ' ' + '𝒳'.charCodeAt(1) ); // 55349 56499 -``` - -...В то время как `codePointAt` возвращает его Unicode-код суррогатной пары правильно: - -```js -//+ run -// один символ с "длинным" (более 2 байт) unicode-кодом -alert( '𝒳'.codePointAt(0) ); // 119987 -``` - -Метод `String.fromCodePoint(code)` корректно создаёт строку из "длинного кода", в отличие от старого `String.fromCharCode(code)`. - -Например: - -```js -//+ run -// Правильно -alert( String.fromCodePoint(119987) ); // 𝒳 -// Неверно! -alert( String.fromCharCode(119987) ); // 풳 -``` - -Более старый метод `fromCharCode` в последней строке дал неверный результат, так как он берёт только первые два байта от числа `119987` и создаёт символ из них, а остальные отбрасывает. - - -### \u{длинный код} - -Есть и ещё синтаксическое улучшение для больших Unicode-кодов. - -В JavaScript-строках давно можно вставлять символы по Unicode-коду, вот так: - -```js -//+ run -alert( "\u2033" ); // ″, символ двойного штриха -``` - -Синтаксис: `\uNNNN`, где `NNNN` -- четырёхзначный шестнадцатиричный код, причём он должен быть ровно четырёхзначным. - -"Лишние" цифры уже не войдут в код, например: - -```js -//+ run -alert( "\u20331" ); // Два символа: символ двойного штриха ″, а затем 1 -``` - -Чтобы вводить более длинные коды символов, добавили запись `\u{NNNNNNNN}`, где `NNNNNNNN` -- максимально восьмизначный (но можно и меньше цифр) код. - -Например: - -```js -//+ run -alert( "\u{20331}" ); // 𠌱, китайский иероглиф с этим кодом -``` - -### Unicode-нормализация - -Во многих языках есть символы, которые получаются как сочетание основного символа и какого-то значка над ним или под ним. - -Например, на основе обычного символа `a` существуют символы: `àáâäãåā`. Самые часто встречающиеся подобные сочетания имеют отдельный юникодный код. Но отнюдь не все. - -Для генерации произвольных сочетаний используются несколько юникодных символов: основа и один или несколько значков. - -Например, если после символа `S` идёт символ "точка сверху" (код `\u0307`), то показано это будет как "S с точкой сверху" `Ṡ`. - -Если нужен ещё значок над той же буквой (или под ней) -- без проблем. Просто добавляем соответствующий символ. - -К примеру, если добавить символ "точка снизу" (код `\u0323`), то будет "S с двумя точками сверху и снизу" `Ṩ` . - -Пример этого символа в JavaScript-строке: - -```js -//+ run -alert("S\u0307\u0323"); // Ṩ -``` - -Такая возможность добавить произвольной букве нужные значки, с одной стороны, необходима, а с другой стороны -- возникает проблемка: можно представить одинаковый с точки зрения визуального отображения и интерпретации символ -- разными сочетаниями Unicode-кодов. - -Вот пример: -```js -//+ run -alert("S\u0307\u0323"); // Ṩ -alert("S\u0323\u0307"); // Ṩ - -alert( "S\u0307\u0323" == "S\u0323\u0307" ); // false -``` - -В первой строке после основы `S` идёт сначала значок "верхняя точка", а потом -- нижняя, во второй -- наоборот. По кодам строки не равны друг другу. Но символ задают один и тот же. - - -С целью разрешить эту ситуацию, существует *юникодная нормализация*, при которой строки приводятся к единому, "нормальному", виду. - -В современном JavaScript это делает метод [str.normalize()](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/normalize). - -```js -//+ run -alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true -``` - -Забавно, что в данной конкретной ситуации `normalize()` приведёт последовательность из трёх символов к одному: [\u1e68 (S с двумя точками)](http://www.fileformat.info/info/unicode/char/1e68/index.htm). - -```js -//+ run -alert( "S\u0307\u0323".normalize().length ); // 1, нормализовало в один символ -alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true -``` - -Это, конечно, не всегда так, просто в данном случае оказалось, что именно такой символ в юникоде уже есть. Если добавить значков, то нормализация уже даст несколько символов. - -Для большинства практических задач информации, данной выше, должно быть вполне достаточно, но если хочется более подробно ознакомиться с вариантами и правилами нормализации -- они описаны в приложении к стандарту юникод [Unicode Normalization Forms](http://www.unicode.org/reports/tr15/). - -## Полезные методы - -Добавлены ряд полезных методов общего назначения: - -
    -
  • [str.includes(s)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes) -- проверяет, включает ли одна строка в себя другую, возвращает `true/false`.
  • -
  • [str.endsWith(s)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith) -- возвращает `true`, если строка `str` заканчивается подстрокой `s`.
  • -
  • [str.startsWith(s)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith) -- возвращает `true`, если строка `str` начинается со строки `s`.
  • -
  • [str.repeat(times)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat) -- повторяет строку `str` `times` раз.
  • -
- -Конечно, всё это можно было сделать при помощи других встроенных методов, но новые методы более удобны. - -## Итого - -Улучшения: -
    -
  • Строки-шаблоны -- для удобного задания строк (многострочных, с переменными), плюс возможность использовать функцию шаблонизации для самостоятельного форматирования.
  • -
  • Юникод -- улучшена работа с суррогатными парами.
  • -
  • Полезные методы для проверок вхождения одной строки в другую.
  • -
\ No newline at end of file diff --git a/1-js/10-es-modern/6-es-object/article.md b/1-js/10-es-modern/6-es-object/article.md deleted file mode 100644 index d298c03b..00000000 --- a/1-js/10-es-modern/6-es-object/article.md +++ /dev/null @@ -1,368 +0,0 @@ - -# Объекты и прототипы - -В этом разделе мы рассмотрим нововведения, которые касаются именно объектов. - -По классам -- чуть позже, в отдельном разделе, оно того заслуживает. - -## Короткое свойство - -Зачастую у нас есть переменные, например, `name` и `isAdmin`, и мы хотим использовать их в объекте. - -При объявлении объекта в этом случае достаточно указать только имя свойства, а значение будет взято из переменной с таким именем. - -Например: - -```js -//+ run -'use strict'; - -let name = "Вася"; -let isAdmin = true; - -*!* -let user = { - name, - isAdmin -}; -*/!* -alert( JSON.stringify(user) ); // {"name": "Вася", "isAdmin": true} -``` - - -## Вычисляемые свойства - -В качестве имени свойства можно использовать выражение, например: - -```js -//+ run -'use strict'; - -let propName = "firstName"; - -let user = { -*!* - [propName]: "Вася" -*/!* -}; - -alert( user.firstName ); // Вася -``` - -Или даже так: - -```js -//+ run -'use strict'; - -let a = "Мой "; -let b = "Зелёный "; -let c = "Крокодил"; - -let user = { -*!* - [(a + b + c).toLowerCase()]: "Вася" -*/!* -}; - -alert( user["мой зелёный крокодил"] ); // Вася -``` - - -## Геттер-сеттер для прототипа - -В ES5 для прототипа был метод-геттер: -
    -
  • `Object.getPrototypeOf(obj)`
  • -
- -В ES-2015 также добавился сеттер: -
    -
  • `Object.setPrototypeOf(obj, newProto)`
  • -
- -...А также "узаконено" свойство `__proto__`, которое даёт прямой доступ к прототипу. Его, в качестве "нестандартного", но удобного способа работы с прототипом реализовали почти все браузеры (кроме IE10-), так что было принято решение добавить его в стандарт. - -## Object.assign - -Функция `Object.assign` получает список объектов и копирует в первый `target` свойства из остальных. - -Синтаксис: -```js -Object.assign(target, src1, src2...) -``` - -При этом последующие свойства перезаписывают предыдущие. - -Например: - -```js -//+ run -'use strict'; - -let user = { name: "Вася" }; -let visitor = { isAdmin: false, visits: true }; -let admin = { isAdmin: true }; - -Object.assign(user, visitor, admin); - -// user <- visitor <- admin -alert( JSON.stringify(user) ); // user: Вася, visits: true, isAdmin: true -``` - -Его также можно использовать для 1-уровневого клонирования объекта: - -```js -'use strict'; - -let user = { name: "Вася", isAdmin: false }; - -*!* -// clone = пустой объект + все свойства user -let clone = Object.assign({}, user); -*/!* -``` - - -## Object.is(value1, value2) - -Новая функция для проверки равенства значений. - -Возвращает `true`, если значения `value1` и `value2` равны, иначе `false`. - -Она похожа на обычное строгое равенство `===`, но есть отличия: - -```js -//+ run - -// Сравнение +0 и -0 -alert( Object.is(+0, -0)); // false -alert( +0 === -0 ); // true - -// Сравнение с NaN -alert( Object.is(NaN, NaN) ); // true -alert( NaN === NaN ); // false -``` - -Отличия эти в большинстве ситуаций некритичны, так что непохоже, чтобы эта функция вытеснила обычную проверку `===`. Что интересно -- этот алгоритм сравнения, который называется [SameValue](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-samevalue), применяется во внутренних реализациях различных методов современного стандарта. - - -## Методы объекта - -Долгое время в JavaScript термин "метод объекта" был просто альтернативным названием для свойства-функции. - -Теперь это уже не так, добавлены именно "методы объекта", которые, по сути, являются свойствами-функциями, привязанными к объекту. - -Их особенности: - -
    -
  1. Более короткий синтаксис объявления.
  2. -
  3. Наличие в методах специального внутреннего свойства `[[HomeObject]]` ("домашний объект"), ссылающегося на объект, которому метод принадлежит. Мы посмотрим его использование чуть дальше.
  4. -
- -Для объявления метода вместо записи `"prop: function() {…}"` нужно написать просто `"prop() { … }"`. - -Например: - -```js -//+ run -'use strict'; - -let name = "Вася"; -let user = { - name, -*!* - // вместо "sayHi: function() {" пишем "sayHi() {" - sayHi() { - alert(this.name); - } -*/!* -}; - -user.sayHi(); // Вася -``` - -Как видно, для создания метода нужно писать меньше букв. Что же касается вызова -- он ничем не отличается от обычной функции. На данном этапе можно считать, что "метод" -- это просто сокращённый синтаксис для свойства-функции. Дополнительные возможности, которые даёт такое объявление, мы рассмотрим позже. - -Также методами станут объявления геттеров `get prop()` и сеттеров `set prop()`: - -```js -//+ run -'use strict'; - -let name = "Вася", surname="Петров"; -let user = { - name, - surname, - get fullName() { - return `${name} ${surname}`; - } -}; - -alert( user.fullName ); // Вася Петров -``` - -Можно задать и метод с вычисляемым названием: - -```js -//+ run -'use strict'; - -let methodName = "getFirstName"; - -let user = { - // в квадратных скобках может быть любое выражение, - // которое должно вернуть название метода - [methodName]() { // вместо [methodName]: function() { - return "Вася"; - } -}; - -alert( user.getFirstName() ); // Вася -``` - -Итак, мы рассмотрели синтаксические улучшения. Если коротко, то не надо писать слово "function". Теперь перейдём к другим отличиям. - -## super - -В ES-2015 появилось новое ключевое слово `super`. Оно предназначено только для использования в методах объекта. - -Вызов `super.parentProperty` позволяет из метода объекта получить свойство его прототипа. - -Например, в коде ниже `rabbit` наследует от `animal`. - -Вызов `super.walk()` из метода объекта `rabbit` обращается к `animal.walk()`: - -```js -//+ run -'use strict'; - -let animal = { - walk() { - alert("I'm walking"); - } -}; - -let rabbit = { - __proto__: animal, - walk() { -*!* - alert(super.walk); // walk() { … } - super.walk(); // I'm walking -*/!* - } -}; - -rabbit.walk(); -``` - -Как правило, это используется в [классах](/class), которые мы рассмотрим в следующем разделе, но важно понимать, что "классы" здесь на самом деле не при чём. Свойство `super` работает через прототип, на уровне методов объекта. - -При обращении через `super` используется `[[HomeObject]]` текущего метода, и от него берётся `__proto__`. Поэтому `super` работает только внутри методов. - -В частности, если переписать этот код, оформив `rabbit.walk` как обычное свойство-функцию, то будет ошибка: - -```js -//+ run -'use strict'; - -let animal = { - walk() { - alert("I'm walking"); - } -}; - -let rabbit = { - __proto__: animal, -*!* - walk: function() { // Надо: walk() { - super.walk(); // Будет ошибка! - } -*/!* -}; - -rabbit.walk(); -``` - -Ошибка возникнет, так как `rabbit.walk` теперь обычная функция, и не имеет `[[HomeObject]]`. В ней не работает `super`. - -Исключением из этого правила являются функции-стрелки. В них используется `super` внешней функции. Например, здесь функция-стрелка в `setTimeout` берёт внешний `super`: - - -```js -//+ run -'use strict'; - -let animal = { - walk() { - alert("I'm walking"); - } -}; - -let rabbit = { - __proto__: animal, - walk() { -*!* - setTimeout(() => super.walk()); // I'm walking -*/!* - } -}; - -rabbit.walk(); -``` - -Ранее мы говорили о том, что у функций-стрелок нет своего `this`, `arguments`: они используют те, которые во внешней функции. Теперь к этому списку добавился ещё и `super`. - -[smart header="Свойство `[[HomeObject]]` -- не изменяемое"] - -При создании метода -- он привязан к своему объекту навсегда. Технически можно даже скопировать его и запустить отдельно, и `super` продолжит работать: - -```js -//+ run -'use strict'; - -let animal = { - walk() { alert("I'm walking"); } -}; - -let rabbit = { - __proto__: animal, - walk() { - super.walk(); - } -}; - -let walk = rabbit.walk; // скопируем метод в переменную -*!* -walk(); // вызовет animal.walk() -// I'm walking -*/!* -``` - -В примере выше метод `walk()` запускается отдельно от объекта, но всё равно сохраняется через `super` доступ к его прототипу, благодаря `[[HomeObject]]`. - -Это -- скорее технический момент, так как методы объекта, всё же, предназначены для вызова в контексте этого объекта. В частности, правила для `this` в методах -- те же, что и для обычных функций. В примере выше при вызове `walk()` без объекта `this` будет `undefined`. -[/smart] - -## Итого - -Улучшения в описании свойств: -
    -
  • Запись `name: name` можно заменить на просто `name`
  • -
  • Если имя свойства находится в переменной или задано выражением `expr`, то его можно указать в квадратных скобках `[expr]`.
  • -
  • Свойства-функции можно оформить как методы: `"prop: function() {"` -> `"prop() {"`.
  • -
- -В методах работает обращение к свойствам прототипа через `super.parentProperty`. - -Для работы с прототипом: -
    -
  • `Object.setPrototypeOf(obj, proto)` -- метод для установки прототипа.
  • -
  • `obj.__proto__` -- ссылка на прототип.
  • -
- -Дополнительно: -
    -
  • Метод `Object.assign(target, src1, src2...)` -- копирует свойства из всех аргументов в первый объект.
  • -
  • Метод `Object.is(value1, value2)` проверяет два значения на равенство. В отличие от `===` считает `+0` и `-0` разными числами. А также считает, что `NaN` равно самому себе.
  • -
diff --git a/1-js/10-es-modern/7-es-class/article.md b/1-js/10-es-modern/7-es-class/article.md deleted file mode 100644 index 8ce0f71b..00000000 --- a/1-js/10-es-modern/7-es-class/article.md +++ /dev/null @@ -1,382 +0,0 @@ - -# Классы - -В современном JavaScript появился новый, "более красивый" синтаксис для классов. - -Новая конструкция `class` -- удобный "синтаксический сахар" для задания конструктора вместе с прототипом. - -## Class - -Синтаксис для классов выглядит так: - -```js -class Название [extends Родитель] { - constructor - методы -} -``` - -Например: - -```js -//+ run -'use strict'; - -class User { - - constructor(name) { - this.name = name; - } - - sayHi() { - alert(this.name); - } - -} - -let user = new User("Вася"); -user.sayHi(); // Вася -``` - -Функция `constructor` запускается при создании `new User`, остальные методы -- записываются в `User.prototype`. - -Это объявление примерно аналогично такому: - -```js -function User(name) { - this.name = name; -} - -User.prototype.sayHi = function() { - alert(this.name); -}; -``` - -В обоих случаях `new User` будет создавать объекты. Метод `sayHi` -- также в обоих случаях находится в прототипе. - -Но при объявлении через `class` есть и ряд отличий: - -
    -
  • `User` нельзя вызывать без `new`, будет ошибка.
  • -
  • Объявление класса с точки зрения области видимости ведёт себя как `let`. В частности, оно видно только текущем в блоке и только в коде, который находится ниже объявления (Function Declaration видно и до объявления).
  • -
- -Методы, объявленные внутри `class`, также имеют ряд особенностей: - -
    -
  • Метод `sayHi` является именно методом, то есть имеет доступ к `super`.
  • -
  • Все методы класса работают в режиме `use strict`, даже если он не указан.
  • -
  • Все методы класса не перечислимы. То есть в цикле `for..in` по объекту их не будет.
  • -
- -## Class Expression - -Так же, как и Function Expression, классы можно задавать "инлайн", в любом выражении и внутри вызова функции. - -Это называется Class Expression: - -```js -//+ run -'use strict'; - -let User = class { - sayHi() { alert('Привет!'); } -}; - -new User().sayHi(); -``` - -В примере выше у класса нет имени, что один-в-один соответствует синтаксису функций. Но имя можно дать. Тогда оно, как и в Named Function Expression, будет доступно только внутри класса: - - -```js -//+ run -'use strict'; - -let SiteGuest = class User { - sayHi() { alert('Привет!'); } -}; - -new SiteGuest().sayHi(); // Привет -*!* -new User(); // ошибка -*/!* -``` - -В примере выше имя `User` будет доступно только внутри класса и может быть использовано, например для создания новых объектов данного типа. - -Наиболее очевидная область применения этой возможности -- создание вспомогательного класса прямо при вызове функции. - -Например, функция `createModel` в примере ниже создаёт объект по классу и данным, добавляет ему `_id` и пишет в "реестр" `allModels`: - -```js -//+ run -'use strict'; - -let allModels = {}; - -function createModel(Model, ...args) { - let model = new Model(...args); - - model._id = Math.random().toString(36).slice(2); - allModels[model._id] = model; - - return model; -} - -let user = createModel(class User { - constructor(name) { - this.name = name; - } - sayHi() { - alert(this.name); - } -}, "Вася"); - -user.sayHi(); // Вася - -alert( allModels[user._id].name ); // Вася -``` - -## Геттеры, сеттеры и вычисляемые свойства - -В классах, как и в обычных объектах, можно объявлять геттеры и сеттеры через `get/set`, а также использовать `[…]` для свойств с вычисляемыми именами: - -```js -//+ run -'use strict'; - -class User { - constructor(firstName, lastName) { - this.firstName = firstName; - this.lastName = lastName; - } - -*!* - // геттер -*/!* - get fullName() { - return `${this.firstName} ${this.lastName}`; - } - -*!* - // сеттер -*/!* - set fullName(newValue) { - [this.firstName, this.lastName] = newValue.split(' '); - } - -*!* - // вычисляемое название метода -*/!* - ["test".toUpperCase()]: true - -}; - -let user = new User("Вася", "Пупков"); -alert( user.fullName ); // Вася Пупков -user.fullName = "Иван Петров"; -alert( user.fullName ); // Иван Петров -alert( user.TEST ); // true -``` - -При чтении `fullName` будет вызван метод `get fullName()`, при присвоении -- метод `set fullName` с новым значением. - -[warn header="`class` не позволяет задавать свойства-значения"] - -В синтаксисе классов, как мы видели выше, можно создавать методы. Они будут записаны в прототип, как например `User.prototype.sayHi`. - -Однако, нет возможности задать в прототипе обычное значение (не функцию), такое как `User.prototype.key = "value"`. - -Конечно, никто не мешает после объявления класса в прототип дописать подобные свойства, однако предполагается, что в прототипе должны быть только методы. - -Если свойство-значение, всё же, необходимо, то, можно создать геттер, который будет нужное значение возвращать. -[/warn] - - -## Статические свойства - -Класс, как и функция, является объектом. Статические свойства класса `User` -- это свойства непосредственно `User`, то есть доступные из него "через точку". - -Для их объявления используется ключевое слово `static`. - -Например: - -```js -//+ run -'use strict'; - -class User { - constructor(firstName, lastName) { - this.firstName = firstName; - this.lastName = lastName; - } - -*!* - static createGuest() { - return new User("Гость", "Сайта"); - } -*/!* -}; - -let user = User.createGuest(); - -alert( user.firstName ); // Гость - -alert( User.createGuest ); // createGuest ... (функция) -``` - -Как правило, они используются для операций, не требующих наличия объекта, например -- для фабричных, как в примере выше, то есть как альтернативные варианты конструктора. Или же, можно добавить метод `User.compare`, который будет сравнивать двух пользователей для целей сортировки. - -Также статическими удобно делать константы: - -```js -//+ run -'use strict'; - -class Menu { - static get elemClass() { - return "menu" - } -} - -alert( Menu.elemClass ); // menu -``` - -## Наследование - -Синтаксис: -```js -class Child extends Parent { - ... -} -``` - -Посмотрим, как это выглядит на практике. В примере ниже объявлено два класса: `Animal` и наследующий от него `Rabbit`: - -```js -//+ run -'use strict'; - -class Animal { - constructor(name) { - this.name = name; - } - - walk() { - alert("I walk: " + this.name); - } -} - -*!* -class Rabbit extends Animal { -*/!* - walk() { - super.walk(); - alert("...and jump!"); - } -} - -new Rabbit("Вася").walk(); -// I walk: Вася -// and jump! -``` - -Как видим, в `new Rabbit` доступны как свои методы, так и (через `super`) методы родителя. - -Это потому, что при наследовании через `extends` формируется стандартная цепочка прототипов: методы `Rabbit` находятся в `Rabbit.prototype`, методы `Animal` -- в `Animal.prototype`, и они связаны через `__proto__`: - -```js -//+ run -'use strict'; - -class Animal { } -class Rabbit extends Animal { } - -alert( Rabbit.prototype.__proto__ == Animal.prototype ); // true -``` - -Как видно из примера выше, методы родителя (`walk`) можно переопределить в наследнике. При этом для обращения к родительскому методу используют `super.walk()`. - -Немного особая история -- с конструктором. - -Конструктор `constructor` родителя наследуется автоматически. То есть, если в потомке не указан свой `constructor`, то используется родительский. В примере выше `Rabbit`, таким образом, использует `constructor` от `Animal`. - -Если же у потомка свой `constructor`, то чтобы в нём вызвать конструктор родителя -- используется синтаксис `super()` с аргументами для родителя. - -Например, вызовем конструктор `Animal` в `Rabbit`: - -```js -//+ run -'use strict'; - -class Animal { - constructor(name) { - this.name = name; - } - - walk() { - alert("I walk: " + this.name); - } -} - -class Rabbit extends Animal { -*!* - constructor() { - // вызвать конструктор Animal с аргументом "Кроль" - super("Кроль"); // то же, что и Animal.call(this, "Кроль") - } -*/!* -} - -new Rabbit().walk(); // I walk: Кроль -``` - -Для такого вызова есть небольшие ограничения: -
    -
  • Вызвать конструктор родителя можно только изнутри конструктора потомка. В частности, `super()` нельзя вызвать из произвольного метода.
  • -
  • В конструкторе потомка мы обязаны вызвать `super()` до обращения к `this`. До вызова `super` не существует `this`, так как по спецификации в этом случае именно `super` инициализует `this`.
  • -
- -Второе ограничение выглядит несколько странно, поэтому проиллюстрируем его примером: - -```js -//+ run -'use strict'; - -class Animal { - constructor(name) { - this.name = name; - } -} - -class Rabbit extends Animal { -*!* - constructor() { - alert(this); // ошибка, this не определён! - // обязаны вызвать super() до обращения к this - super(); - // а вот здесь уже можно использовать this - } -*/!* -} - -new Rabbit(); -``` - - -## Итого - -
    -
  • Классы можно объявлять как в основном потоке кода, так и "инлайн", по аналогии с Function Declaration и Expression.
  • -
  • В объявлении классов можно использовать методы, геттеры/сеттеры и вычислимые названия методов.
  • -
  • При наследовании вызов конструктора родителя осуществлятся через `super(...args)`, вызов родительских методов -- через `super.method(...args)`.
  • -
- -Концепция классов, которая после долгих обсуждений получилась в стандарте EcmaScript, носит название "максимально минимальной". То есть, в неё вошли только те возможности, которые уж точно необходимы. - -В частности, не вошли "приватные" и "защищённые" свойства. То есть, все свойства и методы класса технически доступны снаружи. Возможно, они появятся в будущих редакциях стандарта. - - - - diff --git a/1-js/10-es-modern/8-symbol/article.md b/1-js/10-es-modern/8-symbol/article.md deleted file mode 100644 index 0e89bb9b..00000000 --- a/1-js/10-es-modern/8-symbol/article.md +++ /dev/null @@ -1,210 +0,0 @@ - -# Тип данных Symbol - -Новый примитивный тип данных Symbol служит для создания уникальных идентификаторов. - -Мы вначале рассмотрим объявление и особенности символов, а затем -- их использование. - -## Объявление - -Синтаксис: -```js -let sym = Symbol(); -``` - -Обратим внимание, не `new Symbol`, а просто `Symbol`, так как это -- примитив. - -У символов есть и соответствующий `typeof`: - -```js -//+ run -'use strict'; - -let sym = Symbol(); -alert( typeof sym ); // symbol -``` - - -Каждый символ -- уникален. У функции `Symbol` есть необязательный аргумент "имя символа". Можно его использовать для описания символа, в целях отладки: - -```js -//+ run -'use strict'; - -let sym = Symbol("name"); -alert( sym.toString() ); // Symbol(name) -``` - -...Но при этом если у двух символов одинаковое имя, то это не значит, что они равны: - -```js -//+ run -alert( Symbol("name") == Symbol("name") ); // false -``` - -Если хочется из разных частей программы использовать именно одинаковый символ, то можно передавать между ними объект символа или же -- использовать "глобальные символы" и "реестр глобальных символов", которые мы рассмотрим далее. - -## Глобальные символы - -Существует "глобальный реестр" символов, который позволяет, при необходимости, разделять символы между частями программы. - -Для чтения (или создания, при отсутствии) "глобального" символа служит вызов `Symbol.for(имя)`. - -Например: - -```js -//+ run -'use strict'; - -// создание символа в реестре -let name = Symbol.for("name"); - -// символ уже есть, чтение из реестра -alert( Symbol.for("name") == name ); // true -``` - -Вызов `Symbol.for` возвращает символ по имени. Обратным для него является вызов `Symbol.keyFor(sym)` позволяет получить по глобальному символу его имя: - - -```js -//+ run -'use strict'; - -// создание символа в реестре -let name = Symbol.for("name"); - -// получение имени символа -alert( Symbol.keyFor(name) ); // name -``` -[warn header="`Symbol.keyFor` возвращает `undefined`, если символ не глобальный"] -Заметим, что `Symbol.keyFor` работает *только для глобальных символов*, для остальных будет возвращено `undefined`: - -```js -//+ run -'use strict'; - -alert( Symbol.keyFor(Symbol.for("name")) ); // name, глобальный -alert( Symbol.keyFor(Symbol("name2")) ); // undefined, обычный символ -``` - -Таким образом, имя символа, если этот символ не глобальный, не имеет особого применения, оно полезно лишь в целях вывода и отладки. -[/warn] - -## Использование символов - -Символы можно использовать в качестве имён для свойств объекта, вот так: - -```js -//+ run -'use strict'; - -let isAdmin = Symbol("isAdmin"); - -let user = { - name: "Вася", - [isAdmin]: true -}; - -alert(user[isAdmin]); // true -``` - -Особенность символов -- в том, что если в объект записать свойство-символ, то оно не участвует в итерации: - -```js -//+ run -'use strict'; - -let user = { - name: "Вася", - age: 30, - [Symbol.for("isAdmin")]: true -}; - -// в цикле for..in также не будет символа -alert( Object.keys(user) ); // name, age - -// доступ к свойству через глобальный символ — работает -alert( user[Symbol.for("isAdmin")] ); -``` - -Кроме того, свойство-символ недоступно, если обратиться к его названию: `user.isAdmin` не существует. - -Зачем всё это, почему не просто использовать строки? - -Резонный вопрос. На ум могут прийти соображения производительности, так как символы -- это по сути специальные идентификаторы, они компактнее, чем строка. Но при современных оптимизациях объектов это редко имеет значение. - -Самое широкое применение символов предусмотрено внутри самого стандарта JavaScript. В современном стандарте есть много системных символов. Их список есть в спецификации, в таблице [Well-known Symbols](http://www.ecma-international.org/ecma-262/6.0/index.html#table-1). В спецификации принято символы для краткости обозначать их как '@@имя', например `@@iterator`, но доступны они как свойства `Symbol`. - -Например: -
    -
  • `Symbol.toPrimitive` -- идентификатор для свойства, задающего функцию преобразования объекта в примитив.
  • -
  • `Symbol.iterator` -- идентификатор для свойства, задающего функцию итерации по объекту.
  • -
  • ...и т.п.
  • -
- -**Мы легко поймём смысл введения нового типа "символ", если поставим себя на место создателей языка JavaScript.** - -Допустим, в новом стандарте нам надо добавить к объекту "особый" функционал, например, функцию, которая задаёт преобразование объекта к примитиву. Как `obj.toString`, но для преобразования в примитивы. - -Мы ведь не можем просто сказать, что "свойство obj.toPrimitive теперь будет задавать преобразование к примитиву и автоматически вызываться в таких-то ситуациях". Это опасно. Мы не можем так просто взять и придать особый смысл свойству. Мало ли, вполне возможно, что свойство с таким именем уже используется в существующем коде, и если сделать его особым, то он сломается. - -Нельзя просто взять и зарезервировать какие-то свойства существующих объектов для нового функционала. - -Поэтому ввели целый тип "символы". Их можно использовать для задания таких свойств, так как они: -
    -
  • а) уникальны,
  • -
  • б) не участвуют в циклах,
  • -
  • в) заведомо не сломают старый код, который о них слыхом не слыхивал.
  • -
- -Продемонстрируем отсутствие конфликта для нового системного свойства `Symbol.iterator`: - -```js -//+ run -'use strict'; - -let obj = { - iterator: 1, - [Symbol.iterator]() {} -} - -alert(obj.iterator); // 1 -alert(obj[Symbol.iterator]) // function, символ не конфликтует -``` - -Выше мы использовали системный символ `Symbol.iterator`, поскольку он один из самых широко поддерживаемых. Мы подробно разберём его смысл в главе про [итераторы](/iterator), пока же -- это просто пример символа. - -Чтобы получить все символы объекта, есть особый вызов [Object.getOwnPropertySymbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols). - -Эта функция возвращает все символы в объекте (и только их). Заметим, что старая функция `getOwnPropertyNames` символы не возвращает, что опять же гарантирует отсутствие конфликтов со старым кодом. - -```js -//+ run -'use strict'; - -let obj = { - iterator: 1, - [Symbol.iterator]: function() {} -} - -// один символ в объекте -alert( Object.getOwnPropertySymbols(obj) ); // Symbol(Symbol.iterator) - -// и одно обычное свойство -alert( Object.getOwnPropertyNames(obj) ); // iterator -``` - -## Итого - -
    -
  • Символы -- новый примитивный тип, предназначенный для уникальных идентификаторов.
  • -
  • Все символы уникальны, символы с одинаковым именем не равны друг другу.
  • -
  • Существует глобальный реестр символов, доступных через метод `Symbol.for(name)`. Для глобального символа можно получить имя вызовом и `Symbol.keyFor(sym)`.
  • -
- -Основная область использования символов -- это системные свойства объектов, которые задают разные аспекты их поведения. Поддержка у них пока небольшая, но она растёт. Системные символы позволяют разработчикам стандарта добавлять новые "особые" свойства объектов, при этом не резервируя соответствующие строковые значения. - -Системные символы доступны как свойства функции `Symbol`, например `Symbol.iterator`. - -Мы можем создавать и свои символы, использовать их в объектах. Записывать их как свойства `Symbol`, разумеется, нельзя, если нужен глобально доступный символ, то используется `Symbol.for(имя)`. - diff --git a/1-js/10-es-modern/9-iterator/article.md b/1-js/10-es-modern/9-iterator/article.md deleted file mode 100644 index 4d7e50db..00000000 --- a/1-js/10-es-modern/9-iterator/article.md +++ /dev/null @@ -1,230 +0,0 @@ - -# Итераторы - -В современный JavaScript добавлена новая концепция "итерируемых" (iterable) объектов. - -Итерируемые или, иными словами, "перебираемые" объекты -- это те, содержимое которых можно перебрать в цикле. - -Например, перебираемым объектом является массив. Но не только он. В браузере существует множество объектов, которые не являются массивами, но содержимое которых можно перебрать (к примеру, список DOM-узлов). - -Для перебора таких объектов добавлен новый синтаксис цикла: `for..of`. - -Например: - -```js -//+ run -'use strict'; - -let arr = [1, 2, 3]; // массив — пример итерируемого объекта - -for(let value of arr) { - alert(value); // 1, затем 2, затем 3 -} -``` - -Также итерируемой является строка: - -```js -//+ run -'use strict'; - -for(let char of "Привет") { - alert(char); // Выведет по одной букве: П, р, и, в, е, т -} -``` - - -Итераторы -- расширяющая понятие "массив" концепция, которая пронизывает современный стандарт JavaScript сверху донизу. - -Практически везде, где нужен перебор, он осуществляется через итераторы. Это включает в себя не только строки, массивы, но и вызов функции с оператором spread `f(...args)`, и многое другое. - -В отличие от массивов, "перебираемые" объекты могут не иметь "длины" `length`. Как мы увидим далее, итераторы дают возможность сделать "перебираемыми" любые объекты. - -## Свой итератор - -Допустим, у нас есть некий объект, который надо "умным способом" перебрать. - -Например, `range` -- диапазон чисел от `from` до `to`, и мы хотим, чтобы `for(let num of range)` "перебирал", этот объект. При этом под перебором мы подразумеваем перечисление чисел от `from` до `to`. - -Объект `range` без итератора: - -```js -let range = { - from: 1, - to: 5 -}; - -// хотим сделать перебор -// for(let num of range) ... -``` - -Для возможности использовать объект в `for..of` нужно создать в нём свойство с названием `Symbol.iterator` (системный символ). - -При вызове метода `Symbol.iterator` перебираемый объект должен возвращать другой объект ("итератор"), который умеет осуществлять перебор. - -По стандарту у такого объекта должен быть метод `next()`, который при каждом вызове возвращает очередное значение и окончен ли перебор. - -Так это выглядит в коде: - -```js -//+ run -'use strict'; - -let range = { - from: 1, - to: 5 -} - -// сделаем объект range итерируемым -range[Symbol.iterator] = function() { - - let current = this.from; - let last = this.to; - - // метод должен вернуть объект с next() - return { - next() { - if (current <= last) { - return { - done: false, - value: current++ - }; - } else { - return { - done: true - }; - } - } - - } -}; - -for (let num of range) { - alert(num); // 1, затем 2, 3, 4, 5 -} -``` - -Как видно из кода выше, здесь имеет место разделение сущностей: - -
    -
  • Перебираемый объект `range` сам не реализует методы для своего перебора.
  • -
  • Для этого создаётся другой объект, который хранит текущее состояние перебора и возвращает значение. Этот объект называется итератором и возвращается при вызове метода `range[Symbol.iterator]`.
  • -
  • У итератора должен быть метод `next()`, который при каждом вызове возвращает объект со свойствами: -
      -
    • `value` -- очередное значение, -
    • `done` -- равно `false`, если есть ещё значения, и `true` -- в конце.
    • -
    -
  • -
- -Конструкция `for..of` в начале своего выполнения автоматически вызывает `Symbol.iterator()`, получает итератор и далее вызывает метод `next()` до получения `done: true`. Такова внутренняя механика. Внешний код при переборе через `for..of` видит только значения. - -Такое отделение функционала перебора от самого объекта даёт дополнительную гибкость, например, объект может возвращать разные итераторы в зависимости от своего настроения и времени суток. Однако, бывают ситуации, когда оно не нужно. - -Если функционал по перебору (метод `next`) предоставляется самим объектом, то можно вернуть `this` в качестве итератора: - - -```js -//+ run -'use strict'; - -let range = { - from: 1, - to: 5, - -*!* - [Symbol.iterator]() { - return this; - }, -*/!* - - next() { - if (this.current === undefined) { - // инициализация состояния итерации - this.current = this.from; - } - - if (this.current <= this.to) { - return { - done: false, - value: this.current++ - }; - } else { - // очистка текущей итерации - delete this.current; - return { - done: true - }; - } - } - -}; - -for (let num of range) { - alert(num); // 1, затем 2, 3, 4, 5 -} - -// Произойдёт вызов Math.max(1,2,3,4,5); -alert( Math.max(...range) ); // 5 (*) - -``` - -При таком подходе сам объект и хранит состояние итерации (текущий перебираемый элемент). - -В данном случае это работает, но для большей гибкости и понятности кода рекомендуется, всё же, выделять итератор в отдельный объект со своим состоянием и кодом. - -[smart header="Оператор spread `...` и итераторы"] -В последней строке `(*)` примера выше можно видеть, что итерируемый объект передаётся через spread для `Math.max`. - -При этом `...range` автоматически превращает итерируемый объект в массив. То есть произойдёт цикл `for..of` по `range`, и его результаты будут использованы в качестве списка аргументов. -[/smart] - -[smart header="Бесконечные итераторы"] -Возможны и бесконечные итераторы. Например, пример выше при `range.to = Infinity` будет таковым. Или можно сделать итератор, генерирующий бесконечную последовательность псевдослучайных чисел. Тоже полезно. - -Нет никаких ограничений на `next`, он может возвращать всё новые и новые значения, и это нормально. - -Разумеется, цикл `for..of` по такому итератору тоже будет бесконечным, нужно его прерывать, например, через `break`. -[/smart] - -## Встроенные итераторы - -Встроенные в JavaScript итераторы можно получить и явным образом, без `for..of`, прямым вызовом `Symbol.iterator`. - -Например, этот код получает итератор для строки и вызывает его полностью "вручную": - -```js -//+ run -'use strict'; - -let str = "Hello"; - -// Делает то же, что и -// for(var letter of str) alert(letter); - -let iterator = str[Symbol.iterator](); - -while(true) { - let result = iterator.next(); - if (result.done) break; - alert(result.value); // Выведет все буквы по очереди -} -``` - -То же самое будет работать и для массивов. - -## Итого - -
    -
  • *Итератор* -- объект, предназначенный для перебора другого объекта.
  • -
  • У итератора должен быть метод `next()`, возвращающий `{done: Boolean, value: any}`, где `value` -- очередное значение, а `done: true` в конце.
  • -
  • Метод `Symbol.iterator` предназначен для получения итератора из объекта. Цикл `for..of` делает это автоматически, но можно и вызвать его напрямую.
  • -
  • В современном стандарте есть много мест, где вместо массива используются более абстрактные "итерируемые" (со свойством `Symbol.iterator`) объекты, например оператор spread `...`.
  • -
  • Встроенные объекты, такие как массивы и строки, являются итерируемыми, в соответствии с описанным выше.
  • -
- - - - - - diff --git a/1-js/10-es-modern/index.md b/1-js/10-es-modern/index.md deleted file mode 100644 index 6a5c7f2f..00000000 --- a/1-js/10-es-modern/index.md +++ /dev/null @@ -1,4 +0,0 @@ -# Современные возможности ES-2015 - -Современный стандарт ES-2015 и его расширения для JavaScript. - diff --git a/1-js/2-first-steps/1-hello-world/1-hello-alert/solution.md b/1-js/2-first-steps/1-hello-world/1-hello-alert/solution.md deleted file mode 100644 index b27e40d9..00000000 --- a/1-js/2-first-steps/1-hello-world/1-hello-alert/solution.md +++ /dev/null @@ -1,22 +0,0 @@ -Код страницы: - -```html - - - - - - - - - - - - - - - -``` - diff --git a/1-js/2-first-steps/1-hello-world/1-hello-alert/solution.view/index.html b/1-js/2-first-steps/1-hello-world/1-hello-alert/solution.view/index.html deleted file mode 100644 index 47bb9ca9..00000000 --- a/1-js/2-first-steps/1-hello-world/1-hello-alert/solution.view/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/2-first-steps/1-hello-world/1-hello-alert/task.md b/1-js/2-first-steps/1-hello-world/1-hello-alert/task.md deleted file mode 100644 index 0f7f9375..00000000 --- a/1-js/2-first-steps/1-hello-world/1-hello-alert/task.md +++ /dev/null @@ -1,9 +0,0 @@ -# Выведите alert - -[importance 5] - -Сделайте страницу, которая выводит "Я - JavaScript!". - -Создайте её на диске, откройте в браузере, убедитесь, что всё работает. - -[demo src="solution"] diff --git a/1-js/2-first-steps/1-hello-world/article.md b/1-js/2-first-steps/1-hello-world/article.md deleted file mode 100644 index 739b103b..00000000 --- a/1-js/2-first-steps/1-hello-world/article.md +++ /dev/null @@ -1,104 +0,0 @@ -# Привет, мир! - -В этой статье мы создадим простой скрипт и посмотрим, как он работает. -[cut] -## Тег SCRIPT - -[smart header="А побыстрее?"] -В том (и только в том!) случае, если читатель нетерпелив и уже разрабатывал на JavaScript или имеет достаточно опыта в другом языке программировании, он может не читать каждую статью этого раздела, а перепрыгнуть сразу к главе [](/javascript-specials). Там будет кратко самое основное. - -Если же у вас есть достаточно времени и желание начать с азов, то читайте дальше :) -[/smart] - -Программы на языке JavaScript можно вставить в любое место HTML при помощи тега `SCRIPT`. Например: - -```html - - - - - - - - - - - -

Начало документа...

-*!* - -*/!* - -

...Конец документа

- - - - -``` - -Этот пример использует следующие элементы: - -
-
<script> ... </script>
-
Тег `script` содержит исполняемый код. Предыдущие стандарты HTML требовали обязательного указания атрибута `type`, но сейчас он уже не нужен. Достаточно просто ` -``` - -Браузер, для которого предназначались такие трюки, очень старый Netscape, давно умер. Поэтому в этих комментариях нет нужды. -
-
- -Итак, для вставки скрипта мы просто пишем ` -[/head] \ No newline at end of file diff --git a/1-js/2-first-steps/11-uibasic/1-simple-page/solution.md b/1-js/2-first-steps/11-uibasic/1-simple-page/solution.md deleted file mode 100644 index a70b07e0..00000000 --- a/1-js/2-first-steps/11-uibasic/1-simple-page/solution.md +++ /dev/null @@ -1,30 +0,0 @@ -JS-код: - -```js -//+ demo run -var name = prompt("Ваше имя?", ""); -alert( name ); -``` - -Полная страница: - -```html - - - - - - - - - - - - - - -``` - diff --git a/1-js/2-first-steps/11-uibasic/1-simple-page/task.md b/1-js/2-first-steps/11-uibasic/1-simple-page/task.md deleted file mode 100644 index 6d2ca2e1..00000000 --- a/1-js/2-first-steps/11-uibasic/1-simple-page/task.md +++ /dev/null @@ -1,8 +0,0 @@ -# Простая страница - -[importance 4] - -Создайте страницу, которая спрашивает имя и выводит его. - -[demo /] - diff --git a/1-js/2-first-steps/11-uibasic/article.md b/1-js/2-first-steps/11-uibasic/article.md deleted file mode 100644 index e5c745ce..00000000 --- a/1-js/2-first-steps/11-uibasic/article.md +++ /dev/null @@ -1,111 +0,0 @@ -# Взаимодействие с пользователем: alert, prompt, confirm - - -В этом разделе мы рассмотрим базовые UI операции: `alert`, `prompt` и `confirm`, которые позволяют работать с данными, полученными от пользователя. -[cut] -## alert - -Синтаксис: - -```js -alert(сообщение) -``` - -`alert` выводит на экран окно с сообщением и приостанавливает выполнение скрипта, пока пользователь не нажмёт "ОК". - -```js -//+ run -alert( "Привет" ); -``` - -Окно сообщения, которое выводится, является *модальным окном*. Слово "модальное" означает, что посетитель не может взаимодействовать со страницей, нажимать другие кнопки и т.п., пока не разберётся с окном. В данном случае - пока не нажмёт на "OK". - -## prompt - -Функция prompt принимает два аргумента: - -```js -//+ no-beautify -result = prompt(title, default); -``` - -Она выводит модальное окно с заголовком `title`, полем для ввода текста, заполненным строкой по умолчанию `default` и кнопками OK/CANCEL. - -Пользователь должен либо что-то ввести и нажать OK, либо отменить ввод кликом на CANCEL или нажатием [key Esc] на клавиатуре. - -**Вызов `prompt` возвращает то, что ввёл посетитель -- строку или специальное значение `null`, если ввод отменён.** - -[warn header="Safari 5.1+ не возвращает `null`"] -Единственный браузер, который не возвращает `null` при отмене ввода -- это Safari. При отсутствии ввода он возвращает пустую строку. Предположительно, это ошибка в браузере. - -Если нам важен этот браузер, то пустую строку нужно обрабатывать точно так же, как и `null`, т.е. считать отменой ввода. -[/warn] - -Как и в случае с `alert`, окно `prompt` модальное. - -```js -//+ run -var years = prompt('Сколько вам лет?', 100); - -alert('Вам ' + years + ' лет!') -``` - -[warn header="Всегда указывайте `default`"] -Второй параметр может отсутствовать. Однако при этом IE вставит в диалог значение по умолчанию `"undefined"`. - -Запустите этот код в IE, чтобы понять о чём речь: - -```js -//+ run -var test = prompt("Тест"); -``` - -Поэтому рекомендуется *всегда* указывать второй аргумент: - -```js -//+ run -var test = prompt("Тест", ''); // <-- так лучше -``` - -[/warn] - - -## confirm - -Синтаксис: - -```js -result = confirm(question); -``` - -`confirm` выводит окно с вопросом `question` с двумя кнопками: OK и CANCEL. - -**Результатом будет `true` при нажатии OK и `false` - при CANCEL([key Esc]).** - -Например: - -```js -//+ run -var isAdmin = confirm("Вы - администратор?"); - -alert( isAdmin ); -``` - -## Особенности встроенных функций - -Конкретное место, где выводится модальное окно с вопросом -- обычно это центр браузера, и внешний вид окна выбирает браузер. Разработчик не может на это влиять. - -С одной стороны -- это недостаток, так как нельзя вывести окно в своем, особо красивом, дизайне. - -С другой стороны, преимущество этих функций по сравнению с другими, более сложными методами взаимодействия, которые мы изучим в дальнейшем -- как раз в том, что они очень просты. - -Это самый простой способ вывести сообщение или получить информацию от посетителя. Поэтому их используют в тех случаях, когда простота важна, а всякие "красивости" особой роли не играют. - - -## Резюме - -
    -
  • `alert` выводит сообщение.
  • -
  • `prompt` выводит сообщение и ждёт, пока пользователь введёт текст, а затем возвращает введённое значение или `null`, если ввод отменён (CANCEL/[key Esc]).
  • -
  • `confirm` выводит сообщение и ждёт, пока пользователь нажмёт "OK" или "CANCEL" и возвращает `true/false`.
  • -
diff --git a/1-js/2-first-steps/12-ifelse/1-if-zero-string/solution.md b/1-js/2-first-steps/12-ifelse/1-if-zero-string/solution.md deleted file mode 100644 index 92098c3e..00000000 --- a/1-js/2-first-steps/12-ifelse/1-if-zero-string/solution.md +++ /dev/null @@ -1,13 +0,0 @@ -**Да, выведется,** т.к. внутри `if` стоит строка `"0"`. - -Любая строка, кроме пустой (а здесь она не пустая), в логическом контексте является `true`. - -Можно запустить и проверить: - -```js -//+ run -if ("0") { - alert( 'Привет' ); -} -``` - diff --git a/1-js/2-first-steps/12-ifelse/1-if-zero-string/task.md b/1-js/2-first-steps/12-ifelse/1-if-zero-string/task.md deleted file mode 100644 index 27ce5aee..00000000 --- a/1-js/2-first-steps/12-ifelse/1-if-zero-string/task.md +++ /dev/null @@ -1,12 +0,0 @@ -# if (строка с нулём) - -[importance 5] - -Выведется ли `alert`? - -```js -if ("0") { - alert( 'Привет' ); -} -``` - diff --git a/1-js/2-first-steps/12-ifelse/2-check-standard/ifelse_task2.png b/1-js/2-first-steps/12-ifelse/2-check-standard/ifelse_task2.png deleted file mode 100644 index a0925072..00000000 Binary files a/1-js/2-first-steps/12-ifelse/2-check-standard/ifelse_task2.png and /dev/null differ diff --git a/1-js/2-first-steps/12-ifelse/2-check-standard/ifelse_task2/index.html b/1-js/2-first-steps/12-ifelse/2-check-standard/ifelse_task2/index.html deleted file mode 100644 index 392ead19..00000000 --- a/1-js/2-first-steps/12-ifelse/2-check-standard/ifelse_task2/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/1-js/2-first-steps/12-ifelse/2-check-standard/solution.md b/1-js/2-first-steps/12-ifelse/2-check-standard/solution.md deleted file mode 100644 index aacb5966..00000000 --- a/1-js/2-first-steps/12-ifelse/2-check-standard/solution.md +++ /dev/null @@ -1,6 +0,0 @@ - - -```html - -``` - diff --git a/1-js/2-first-steps/12-ifelse/2-check-standard/task.md b/1-js/2-first-steps/12-ifelse/2-check-standard/task.md deleted file mode 100644 index f5aa5eb6..00000000 --- a/1-js/2-first-steps/12-ifelse/2-check-standard/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Проверка стандарта - -[importance 2] - -Используя конструкцию `if..else`, напишите код, который будет спрашивать: "Каково "официальное" название JavaScript?". - -Если посетитель вводит "EcmaScript", то выводить "Верно!", если что-то другое -- выводить "Не знаете? "EcmaScript"!". - -Блок-схема: - - - -[demo src="ifelse_task2"] \ No newline at end of file diff --git a/1-js/2-first-steps/12-ifelse/2-check-standardifelse_task2/index.html b/1-js/2-first-steps/12-ifelse/2-check-standardifelse_task2/index.html deleted file mode 100644 index 392ead19..00000000 --- a/1-js/2-first-steps/12-ifelse/2-check-standardifelse_task2/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/1-js/2-first-steps/12-ifelse/3-sign/if_sign/index.html b/1-js/2-first-steps/12-ifelse/3-sign/if_sign/index.html deleted file mode 100644 index 31d37ebc..00000000 --- a/1-js/2-first-steps/12-ifelse/3-sign/if_sign/index.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/2-first-steps/12-ifelse/3-sign/solution.md b/1-js/2-first-steps/12-ifelse/3-sign/solution.md deleted file mode 100644 index df54e347..00000000 --- a/1-js/2-first-steps/12-ifelse/3-sign/solution.md +++ /dev/null @@ -1,15 +0,0 @@ - - -```js -//+ run -var value = prompt('Введите число', 0); - -if (value > 0) { - alert( 1 ); -} else if (value < 0) { - alert( -1 ); -} else { - alert( 0 ); -} -``` - diff --git a/1-js/2-first-steps/12-ifelse/3-sign/task.md b/1-js/2-first-steps/12-ifelse/3-sign/task.md deleted file mode 100644 index 9d24ede9..00000000 --- a/1-js/2-first-steps/12-ifelse/3-sign/task.md +++ /dev/null @@ -1,12 +0,0 @@ -# Получить знак числа - -[importance 2] - -Используя конструкцию `if..else`, напишите код, который получает значение `prompt`, а затем выводит `alert`: -
    -
  • `1`, если значение больше нуля,
  • -
  • `-1`, если значение меньше нуля,
  • -
  • `0`, если значение равно нулю.
  • -
- -[demo src="if_sign"] \ No newline at end of file diff --git a/1-js/2-first-steps/12-ifelse/4-check-login/ifelse_task.png b/1-js/2-first-steps/12-ifelse/4-check-login/ifelse_task.png deleted file mode 100644 index 56ed3c7c..00000000 Binary files a/1-js/2-first-steps/12-ifelse/4-check-login/ifelse_task.png and /dev/null differ diff --git a/1-js/2-first-steps/12-ifelse/4-check-login/solution.md b/1-js/2-first-steps/12-ifelse/4-check-login/solution.md deleted file mode 100644 index edcdbb50..00000000 --- a/1-js/2-first-steps/12-ifelse/4-check-login/solution.md +++ /dev/null @@ -1,33 +0,0 @@ - - -```js -//+ run demo -var userName = prompt('Кто пришёл?', ''); - -if (userName == 'Админ') { - - var pass = prompt('Пароль?', ''); - - if (pass == 'Чёрный Властелин') { - alert( 'Добро пожаловать!' ); - } else if (pass == null) { // (*) - alert( 'Вход отменён' ); - } else { - alert( 'Пароль неверен' ); - } - -} else if (userName == null) { // (**) - alert( 'Вход отменён' ); - -} else { - - alert( 'Я вас не знаю' ); - -} -``` - -Обратите внимание на проверку `if` в строках `(*)` и `(**)`. Везде, кроме Safari, нажатие "Отмена" возвращает `null`, а вот Safari возвращает при отмене пустую строку, поэтому в браузере Safari можно было бы добавить дополнительную проверку на неё. - -Впрочем, такое поведение Safari является некорректным, надеемся, что скоро его исправят. - -Кроме того, обратите внимание на дополнительные вертикальные отступы внутри `if`. Они не обязательны, но полезны для лучшей читаемости кода. \ No newline at end of file diff --git a/1-js/2-first-steps/12-ifelse/4-check-login/task.md b/1-js/2-first-steps/12-ifelse/4-check-login/task.md deleted file mode 100644 index a158cb4b..00000000 --- a/1-js/2-first-steps/12-ifelse/4-check-login/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# Проверка логина - -[importance 3] - -Напишите код, который будет спрашивать логин (`prompt`). - -Если посетитель вводит "Админ", то спрашивать пароль, если нажал отмена (escape) -- выводить "Вход отменён", если вводит что-то другое -- "Я вас не знаю". - -Пароль проверять так. Если введён пароль "Чёрный Властелин", то выводить "Добро пожаловать!", иначе -- "Пароль неверен", при отмене -- "Вход отменён". - -Блок-схема: - - - -Для решения используйте вложенные блоки `if`. Обращайте внимание на стиль и читаемость кода. - -[demo /] \ No newline at end of file diff --git a/1-js/2-first-steps/12-ifelse/5-rewrite-if-question/solution.md b/1-js/2-first-steps/12-ifelse/5-rewrite-if-question/solution.md deleted file mode 100644 index 1799c70b..00000000 --- a/1-js/2-first-steps/12-ifelse/5-rewrite-if-question/solution.md +++ /dev/null @@ -1,6 +0,0 @@ - - -```js -result = (a + b < 4) ? 'Мало' : 'Много'; -``` - diff --git a/1-js/2-first-steps/12-ifelse/5-rewrite-if-question/task.md b/1-js/2-first-steps/12-ifelse/5-rewrite-if-question/task.md deleted file mode 100644 index dc75b1fb..00000000 --- a/1-js/2-first-steps/12-ifelse/5-rewrite-if-question/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Перепишите 'if' в '?' - -[importance 5] - -Перепишите `if` с использованием оператора `'?'`: - -```js -if (a + b < 4) { - result = 'Мало'; -} else { - result = 'Много'; -} -``` - diff --git a/1-js/2-first-steps/12-ifelse/6-rewrite-if-else-question/solution.md b/1-js/2-first-steps/12-ifelse/6-rewrite-if-else-question/solution.md deleted file mode 100644 index 42b7b628..00000000 --- a/1-js/2-first-steps/12-ifelse/6-rewrite-if-else-question/solution.md +++ /dev/null @@ -1,9 +0,0 @@ - - -```js -var message = (login == 'Вася') ? 'Привет' : - (login == 'Директор') ? 'Здравствуйте' : - (login == '') ? 'Нет логина' : - ''; -``` - diff --git a/1-js/2-first-steps/12-ifelse/6-rewrite-if-else-question/task.md b/1-js/2-first-steps/12-ifelse/6-rewrite-if-else-question/task.md deleted file mode 100644 index b3babf08..00000000 --- a/1-js/2-first-steps/12-ifelse/6-rewrite-if-else-question/task.md +++ /dev/null @@ -1,22 +0,0 @@ -# Перепишите 'if..else' в '?' - -[importance 5] - -Перепишите `if..else` с использованием нескольких операторов `'?'`. - -Для читаемости -- оформляйте код в несколько строк. - -```js -var message; - -if (login == 'Вася') { - message = 'Привет'; -} else if (login == 'Директор') { - message = 'Здравствуйте'; -} else if (login == '') { - message = 'Нет логина'; -} else { - message = ''; -} -``` - diff --git a/1-js/2-first-steps/12-ifelse/article.md b/1-js/2-first-steps/12-ifelse/article.md deleted file mode 100644 index 4b33eb99..00000000 --- a/1-js/2-first-steps/12-ifelse/article.md +++ /dev/null @@ -1,233 +0,0 @@ -# Условные операторы: if, '?' - -Иногда, в зависимости от условия, нужно выполнить различные действия. Для этого используется оператор `if`. -[cut] -Например: - -```js -//+ run -var year = prompt('В каком году появилась спецификация ECMA-262 5.1?', ''); - -if (year != 2011) alert( 'А вот и неправильно!' ); -``` - -## Оператор if - -Оператор `if` ("если") получает условие, в примере выше это `year != 2011`. Он вычисляет его, и если результат -- `true`, то выполняет команду. - -Если нужно выполнить более одной команды -- они оформляются блоком кода в фигурных скобках: - -```js -if (year != 2011) { - alert( 'А вот..' ); - alert( '..и неправильно!' ); -} -``` - -**Рекомендуется использовать фигурные скобки всегда, даже когда команда одна.** - -Это улучшает читаемость кода. - - -## Преобразование к логическому типу - -Оператор `if (...)` вычисляет и преобразует выражение в скобках к логическому типу. - -В логическом контексте: -
    -
  • Число `0`, пустая строка `""`, `null` и `undefined`, а также `NaN` являются `false`,
  • -
  • Остальные значения -- `true`.
  • -
- -Например, такое условие никогда не выполнится: - -```js -if (0) { // 0 преобразуется к false - ... -} -``` - -...А такое -- выполнится всегда: - -```js -if (1) { // 1 преобразуется к true - ... -} -``` - -Можно и просто передать уже готовое логическое значение, к примеру, заранее вычисленное в переменной: - -```js -var cond = (year != 2011); // true/false - -if (cond) { - ... -} -``` - -## Неверное условие, else - -Необязательный блок `else` ("иначе") выполняется, если условие неверно: - -```js -//+ run -var year = prompt('Введите год появления стандарта ECMA-262 5.1', ''); - -if (year == 2011) { - alert( 'Да вы знаток!' ); -} else { - alert( 'А вот и неправильно!' ); // любое значение, кроме 2011 -} -``` - -## Несколько условий, else if - -Бывает нужно проверить несколько вариантов условия. Для этого используется блок `else if ...`. Например: - -```js -//+ run -var year = prompt('В каком году появилась спецификация ECMA-262 5.1?', ''); - -if (year < 2011) { - alert( 'Это слишком рано..' ); -} else if (year > 2011) { - alert( 'Это поздновато..' ); -} else { - alert( 'Да, точно в этом году!' ); -} -``` - -В примере выше JavaScript сначала проверит первое условие, если оно ложно -- перейдет ко второму -- и так далее, до последнего `else`. - - -## Оператор вопросительный знак '?' -Иногда нужно в зависимости от условия присвоить переменную. Например: - -```js -//+ run no-beautify -var access; -var age = prompt('Сколько вам лет?', ''); - -*!* -if (age > 14) { - access = true; -} else { - access = false; -} -*/!* - -alert(access); -``` - -Оператор вопросительный знак `'?'` позволяет делать это короче и проще. - -Он состоит из трех частей: - -```js -условие ? значение1 : значение2 -``` - -Проверяется условие, затем если оно верно -- возвращается `значение1`, если неверно -- `значение2`, например: - -```js -access = (age > 14) ? true : false; -``` - -Оператор `'?'` выполняется позже большинства других, в частности -- позже сравнений, поэтому скобки можно не ставить: - -```js -access = age > 14 ? true : false; -``` - -...Но когда скобки есть -- код лучше читается. Так что рекомендуется их писать. - -[smart] -В данном случае можно было бы обойтись и без оператора `'?'`, т.к. сравнение само по себе уже возвращает `true/false`: - -```js -access = age > 14; -``` -[/smart] - -[smart header="\"Тернарный оператор\""] -Вопросительный знак -- единственный оператор, у которого есть аж три аргумента, в то время как у обычных операторов их один-два. -Поэтому его называют *"тернарный оператор"*. -[/smart] - - -## Несколько операторов '?' - -Последовательность операторов `'?'` позволяет вернуть значение в зависимости не от одного условия, а от нескольких. - -Например: -```js -//+ run -var age = prompt('возраст?', 18); - -var message = (age < 3) ? 'Здравствуй, малыш!' : - (age < 18) ? 'Привет!' : - (age < 100) ? 'Здравствуйте!' : - 'Какой необычный возраст!'; - -alert( message ); -``` - -Поначалу может быть сложно понять, что происходит. Однако, внимательно приглядевшись, мы замечаем, что это обычная последовательная проверка! - -Вопросительный знак проверяет сначала `age < 3`, если верно -- возвращает `'Здравствуй, малыш!'`, если нет -- идет за двоеточие и проверяет `age < 18`. Если это верно -- возвращает `'Привет!'`, иначе проверка `age < 100` и `'Здравствуйте!'`... И наконец, если ничего из этого не верно, то `'Какой необычный возраст!'`. - -То же самое через `if..else`: - -```js -if (age < 3) { - message = 'Здравствуй, малыш!'; -} else if (a < 18) { - message = 'Привет!'; -} else if (age < 100) { - message = 'Здравствуйте!'; -} else { - message = 'Какой необычный возраст!'; -} -``` - -## Нетрадиционное использование '?' - -Иногда оператор вопросительный знак `'?'` используют как замену `if`: - -```js -//+ run no-beautify -var company = prompt('Какая компания создала JavaScript?', ''); - -*!* -(company == 'Netscape') ? - alert('Да, верно') : alert('Неправильно'); -*/!* -``` - -Работает это так: в зависимости от условия, будет выполнена либо первая, либо вторая часть после `'?'`. - -Результат выполнения не присваивается в переменную, так что пропадёт (впрочем, `alert` ничего не возвращает). - -**Рекомендуется не использовать вопросительный знак таким образом.** - -Несмотря на то, что с виду такая запись короче `if`, она является существенно менее читаемой. - -Вот, для сравнения, то же самое с `if`: - -```js -//+ run no-beautify -var company = prompt('Какая компания создала JavaScript?', ''); - -*!* -if (company == 'Netscape') { - alert('Да, верно'); -} else { - alert('Неправильно'); -} -*/!* -``` - -При чтении кода глаз идёт вертикально и конструкции, занимающие несколько строк, с понятной вложенностью, воспринимаются гораздо легче. Возможно, вы и сами почувствуете, пробежавшись глазами, что синтаксис с `if` более прост и очевиден чем с оператором `'?'`. - -Смысл оператора `'?'` -- вернуть то или иное значение, в зависимости от условия. Пожалуйста, используйте его по назначению, а для выполнения разных веток кода есть `if`. - diff --git a/1-js/2-first-steps/13-logical-ops/1-alert-null-2-undefined/solution.md b/1-js/2-first-steps/13-logical-ops/1-alert-null-2-undefined/solution.md deleted file mode 100644 index 4756a6ce..00000000 --- a/1-js/2-first-steps/13-logical-ops/1-alert-null-2-undefined/solution.md +++ /dev/null @@ -1,7 +0,0 @@ -Ответ: `2`, это первое значение, которое в логическом контексте даст `true`. - -```js -//+ run -alert( null || 2 || undefined ); -``` - diff --git a/1-js/2-first-steps/13-logical-ops/1-alert-null-2-undefined/task.md b/1-js/2-first-steps/13-logical-ops/1-alert-null-2-undefined/task.md deleted file mode 100644 index f6e93650..00000000 --- a/1-js/2-first-steps/13-logical-ops/1-alert-null-2-undefined/task.md +++ /dev/null @@ -1,10 +0,0 @@ -# Что выведет alert (ИЛИ)? - -[importance 5] - -Что выведет код ниже? - -```js -alert( null || 2 || undefined ); -``` - diff --git a/1-js/2-first-steps/13-logical-ops/2-alert-or/solution.md b/1-js/2-first-steps/13-logical-ops/2-alert-or/solution.md deleted file mode 100644 index 30fce045..00000000 --- a/1-js/2-first-steps/13-logical-ops/2-alert-or/solution.md +++ /dev/null @@ -1,15 +0,0 @@ -Ответ: сначала `1`, затем `2`. - -```js -//+ run -alert( alert(1) || 2 || alert(3) ); -``` - -Вызов `alert` не возвращает значения, или, иначе говоря, возвращает `undefined`. - -
    -
  1. Первый оператор ИЛИ `||` выполнит первый `alert(1)`, получит `undefined` и пойдёт дальше, ко второму операнду.
  2. -
  3. Так как второй операнд `2` является истинным, то вычисления завершатся, результатом `undefined || 2` будет `2`, которое будет выведено внешним `alert( .... )`.
  4. -
- -Второй оператор `||` не будет выполнен, выполнение до `alert(3)` не дойдёт, поэтому `3` выведено не будет. \ No newline at end of file diff --git a/1-js/2-first-steps/13-logical-ops/2-alert-or/task.md b/1-js/2-first-steps/13-logical-ops/2-alert-or/task.md deleted file mode 100644 index 1d4ed593..00000000 --- a/1-js/2-first-steps/13-logical-ops/2-alert-or/task.md +++ /dev/null @@ -1,10 +0,0 @@ -# Что выведет alert (ИЛИ)? - -[importance 3] - -Что выведет код ниже? - -```js -alert( alert(1) || 2 || alert(3) ); -``` - diff --git a/1-js/2-first-steps/13-logical-ops/3-alert-1-null-2/solution.md b/1-js/2-first-steps/13-logical-ops/3-alert-1-null-2/solution.md deleted file mode 100644 index c91e674a..00000000 --- a/1-js/2-first-steps/13-logical-ops/3-alert-1-null-2/solution.md +++ /dev/null @@ -1,7 +0,0 @@ -Ответ: `null`, это первое ложное значение из списка. - -```js -//+ run -alert( 1 && null && 2 ); -``` - diff --git a/1-js/2-first-steps/13-logical-ops/3-alert-1-null-2/task.md b/1-js/2-first-steps/13-logical-ops/3-alert-1-null-2/task.md deleted file mode 100644 index 38fee457..00000000 --- a/1-js/2-first-steps/13-logical-ops/3-alert-1-null-2/task.md +++ /dev/null @@ -1,10 +0,0 @@ -# Что выведет alert (И)? - -[importance 5] - -Что выведет код ниже? - -```js -alert( 1 && null && 2 ); -``` - diff --git a/1-js/2-first-steps/13-logical-ops/4-alert-and/solution.md b/1-js/2-first-steps/13-logical-ops/4-alert-and/solution.md deleted file mode 100644 index 83a88c73..00000000 --- a/1-js/2-first-steps/13-logical-ops/4-alert-and/solution.md +++ /dev/null @@ -1,10 +0,0 @@ -Ответ: `1`, а затем `undefined`. - -```js -//+ run -alert( alert(1) && alert(2) ); -``` - -Вызов `alert` не возвращает значения, или, иначе говоря, возвращает `undefined`. - -Поэтому до правого `alert` дело не дойдёт, вычисления закончатся на левом. \ No newline at end of file diff --git a/1-js/2-first-steps/13-logical-ops/4-alert-and/task.md b/1-js/2-first-steps/13-logical-ops/4-alert-and/task.md deleted file mode 100644 index 2d1594d9..00000000 --- a/1-js/2-first-steps/13-logical-ops/4-alert-and/task.md +++ /dev/null @@ -1,10 +0,0 @@ -# Что выведет alert (И)? - -[importance 3] - -Что выведет код ниже? - -```js -alert( alert(1) && alert(2) ); -``` - diff --git a/1-js/2-first-steps/13-logical-ops/5-alert-and-or/solution.md b/1-js/2-first-steps/13-logical-ops/5-alert-and-or/solution.md deleted file mode 100644 index e02d8e49..00000000 --- a/1-js/2-first-steps/13-logical-ops/5-alert-and-or/solution.md +++ /dev/null @@ -1,16 +0,0 @@ -Ответ: `3`. - -```js -//+ run -alert( null || 2 && 3 || 4 ); -``` - -Приоритет оператора `&&` выше, чем `||`, поэтому он выполнится первым. - -Последовательность вычислений: -``` -null || 2 && 3 || 4 -null || 3 || 4 -3 -``` - diff --git a/1-js/2-first-steps/13-logical-ops/5-alert-and-or/task.md b/1-js/2-first-steps/13-logical-ops/5-alert-and-or/task.md deleted file mode 100644 index 9e367c3b..00000000 --- a/1-js/2-first-steps/13-logical-ops/5-alert-and-or/task.md +++ /dev/null @@ -1,10 +0,0 @@ -# Что выведет этот код? - -[importance 5] - -Что выведет код ниже? - -```js -alert( null || 2 && 3 || 4 ); -``` - diff --git a/1-js/2-first-steps/13-logical-ops/6-check-if-in-range/task.md b/1-js/2-first-steps/13-logical-ops/6-check-if-in-range/task.md deleted file mode 100644 index df16ad99..00000000 --- a/1-js/2-first-steps/13-logical-ops/6-check-if-in-range/task.md +++ /dev/null @@ -1,7 +0,0 @@ -# Проверка if внутри диапазона - -[importance 3] - -Напишите условие `if` для проверки того факта, что переменная `age` находится между `14` и `90` включительно. - -"Включительно" означает, что концы промежутка включены, то есть `age` может быть равна `14` или `90`. \ No newline at end of file diff --git a/1-js/2-first-steps/13-logical-ops/7-check-if-out-range/solution.md b/1-js/2-first-steps/13-logical-ops/7-check-if-out-range/solution.md deleted file mode 100644 index 3d2eab1a..00000000 --- a/1-js/2-first-steps/13-logical-ops/7-check-if-out-range/solution.md +++ /dev/null @@ -1,12 +0,0 @@ -Первый вариант: - -```js -if (!(age >= 14 && age <= 90)) -``` - -Второй вариант: - -```js -if (age < 14 || age > 90) -``` - diff --git a/1-js/2-first-steps/13-logical-ops/7-check-if-out-range/task.md b/1-js/2-first-steps/13-logical-ops/7-check-if-out-range/task.md deleted file mode 100644 index 1d833664..00000000 --- a/1-js/2-first-steps/13-logical-ops/7-check-if-out-range/task.md +++ /dev/null @@ -1,7 +0,0 @@ -# Проверка if вне диапазона - -[importance 3] - -Напишите условие `if` для проверки того факта, что `age` НЕ находится между 14 и 90 включительно. - -Сделайте два варианта условия: первый с использованием оператора НЕ `!`, второй - без этого оператора. \ No newline at end of file diff --git a/1-js/2-first-steps/13-logical-ops/8-if-question/solution.md b/1-js/2-first-steps/13-logical-ops/8-if-question/solution.md deleted file mode 100644 index f1b83dab..00000000 --- a/1-js/2-first-steps/13-logical-ops/8-if-question/solution.md +++ /dev/null @@ -1,21 +0,0 @@ -Ответ: первое и третье выполнятся. - -Детали: - -```js -//+ run -// Выполнится -// Результат -1 || 0 = -1, в логическом контексте true -if (-1 || 0) alert( 'первое' ); - -// Не выполнится -// -1 && 0 = 0, в логическом контексте false -if (-1 && 0) alert( 'второе' ); - -// Выполнится -// оператор && имеет больший приоритет, чем || -// так что -1 && 1 выполнится раньше -// вычисления: null || -1 && 1 -> null || 1 -> 1 -if (null || -1 && 1) alert( 'третье' ); -``` - diff --git a/1-js/2-first-steps/13-logical-ops/8-if-question/task.md b/1-js/2-first-steps/13-logical-ops/8-if-question/task.md deleted file mode 100644 index ee7018b6..00000000 --- a/1-js/2-first-steps/13-logical-ops/8-if-question/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Вопрос про "if" - -[importance 5] - -Какие из этих `if` верны, т.е. выполнятся? - -Какие конкретно значения будут результатами выражений в условиях `if(...)`? - -```js -if (-1 || 0) alert( 'первое' ); -if (-1 && 0) alert( 'второе' ); -if (null || -1 && 1) alert( 'третье' ); -``` - diff --git a/1-js/2-first-steps/13-logical-ops/article.md b/1-js/2-first-steps/13-logical-ops/article.md deleted file mode 100644 index c81990db..00000000 --- a/1-js/2-first-steps/13-logical-ops/article.md +++ /dev/null @@ -1,282 +0,0 @@ -# Логические операторы - -Для операций над логическими значениями в JavaScript есть `||` (ИЛИ), `&&` (И) и `!` (НЕ). - -Хоть они и называются *"логическими"*, но в JavaScript могут применяться к значениям любого типа и возвращают также значения любого типа. -[cut] - -## || (ИЛИ) - -Оператор ИЛИ выглядит как двойной символ вертикальной черты: - -```js -result = a || b; -``` - -Логическое ИЛИ в классическом программировании работает следующим образом: "если *хотя бы один* из аргументов `true`, то возвращает `true`, иначе -- `false`". В JavaScript, как мы увидим далее, это не совсем так, но для начала рассмотрим только логические значения. - -Получается следующая "таблица результатов": - -```js -//+ run -alert( true || true ); // true -alert( false || true ); // true -alert( true || false ); // true -alert( false || false ); // false -``` - -Если значение не логического типа -- то оно к нему приводится в целях вычислений. Например, число `1` будет воспринято как `true`, а `0` -- как `false`: - -```js -//+ run -if (1 || 0) { // сработает как if( true || false ) - alert( 'верно' ); -} -``` - -Обычно оператор ИЛИ используется в `if`, чтобы проверить, выполняется ли хотя бы одно из условий, например: - -```js -//+ run -var hour = 9; - -*!* -if (hour < 10 || hour > 18) { -*/!* - alert( 'Офис до 10 или после 18 закрыт' ); -} -``` - -Можно передать и больше условий: - -```js -//+ run -var hour = 12, - isWeekend = true; - -if (hour < 10 || hour > 18 || isWeekend) { - alert( 'Офис до 10 или после 18 или в выходной закрыт' ); -} -``` - -## Короткий цикл вычислений - - -JavaScript вычисляет несколько ИЛИ слева направо. При этом, чтобы экономить ресурсы, используется так называемый *"короткий цикл вычисления"*. - -Допустим, вычисляются несколько ИЛИ подряд: `a || b || c || ...`. Если первый аргумент -- `true`, то результат заведомо будет `true` (хотя бы одно из значений -- `true`), и остальные значения игнорируются. - -Это особенно заметно, когда выражение, переданное в качестве второго аргумента, имеет *сторонний эффект* -- например, присваивает переменную. - -При запуске примера ниже присвоение `x` не произойдёт: - -```js -//+ run no-beautify -var x; - -*!*true*/!* || (x = 1); - -alert(x); // undefined, x не присвоен -``` - -...А в примере ниже первый аргумент -- `false`, так что ИЛИ попытается вычислить второй, запустив тем самым присваивание: - -```js -//+ run no-beautify -var x; - -*!*false*/!* || (x = 1); - -alert(x); // 1 -``` - -## Значение ИЛИ - -[quote author="Илья Канатов, участник курса JavaScript"] -`||` запинается на "правде",
-`&&` запинается на "лжи". -[/quote] - -Итак, как мы видим, оператор ИЛИ вычисляет ровно столько значений, сколько необходимо -- до первого `true`. - -При этом оператор ИЛИ возвращает то значение, на котором остановились вычисления. Причём, не преобразованное к логическому типу. - -Например: - -```js -//+ run -alert( 1 || 0 ); // 1 -alert( true || 'неважно что' ); // true - -alert( null || 1 ); // 1 -alert( undefined || 0 ); // 0 -``` - -Это используют, в частности, чтобы выбрать первое "истинное" значение из списка: - -```js -//+ run -var undef; // переменная не присвоена, т.е. равна undefined -var zero = 0; -var emptyStr = ""; -var msg = "Привет!"; - -*!* -var result = undef || zero || emptyStr || msg || 0; -*/!* - -alert( result ); // выведет "Привет!" - первое значение, которое является true -``` - -Если все значения "ложные", то `||` возвратит последнее из них: - -```js -//+ run -alert( undefined || '' || false || 0 ); // 0 -``` - -Итак, оператор `||` вычисляет операнды слева направо до первого "истинного" и возвращает его, а если все ложные -- то последнее значение. - -Иначе можно сказать, что "`||` запинается на правде". - -## && (И) - - -Оператор И пишется как два амперсанда `&&`: - -```js -result = a && b; -``` - -В классическом программировании И возвращает `true`, если оба аргумента истинны, а иначе -- `false`: - -```js -//+ run -alert( true && true ); // true -alert( false && true ); // false -alert( true && false ); // false -alert( false && false ); // false -``` - -Пример c `if`: - -```js -//+ run -var hour = 12, - minute = 30; - -if (hour == 12 && minute == 30) { - alert( 'Время 12:30' ); -} -``` - -Как и в ИЛИ, в И допустимы любые значения: - -```js -//+ run -if (1 && 0) { // вычислится как true && false - alert( 'не сработает, т.к. условие ложно' ); -} -``` - -К И применим тот же принцип "короткого цикла вычислений", но немного по-другому, чем к ИЛИ. - -Если левый аргумент -- `false`, оператор И возвращает его и заканчивает вычисления. Иначе -- вычисляет и возвращает правый аргумент. - -Например: - -```js -//+ run -// Первый аргумент - true, -// Поэтому возвращается второй аргумент -alert( 1 && 0 ); // 0 -alert( 1 && 5 ); // 5 - -// Первый аргумент - false, -// Он и возвращается, а второй аргумент игнорируется -alert( null && 5 ); // null -alert( 0 && "не важно" ); // 0 -``` - -Можно передать и несколько значений подряд, при этом возвратится первое "ложное" (на котором остановились вычисления), а если его нет -- то последнее: - -```js -//+ run -alert( 1 && 2 && null && 3 ); // null - -alert( 1 && 2 && 3 ); // 3 -``` - -Итак, оператор `&&` вычисляет операнды слева направо до первого "ложного" и возвращает его, а если все истинные -- то последнее значение. - -Иначе можно сказать, что "`&&` запинается на лжи". - -[smart header="Приоритет у `&&` больше, чем у `||`"] -Приоритет оператора И `&&` больше, чем ИЛИ `||`, так что он выполняется раньше. - -Поэтому в следующем коде сначала будет вычислено правое И: `1 && 0 = 0`, а уже потом -- ИЛИ. - -```js -//+ run -alert( 5 || 1 && 0 ); // 5 -``` -[/smart] - -[warn header="Не используйте `&&` вместо `if`"] - -Оператор `&&` в простых случаях можно использовать вместо `if`, например: - -```js -//+ run -var x = 1; - -(x > 0) && alert( 'Больше' ); -``` - -Действие в правой части `&&` выполнится только в том случае, если до него дойдут вычисления. То есть, `alert` сработает, если в левой части будет `true`. - -Получился аналог: - -```js -//+ run -var x = 1; - -if (x > 0) { - alert( 'Больше' ); -} -``` - -Однако, как правило, вариант с `if` лучше читается и воспринимается. Он более очевиден, поэтому лучше использовать его. Это, впрочем, относится и к другим неочевидным применениям возможностей языка. -[/warn] - -## ! (НЕ) - -Оператор НЕ -- самый простой. Он получает один аргумент. Синтаксис: - -```js -var result = !value; -``` - -Действия `!`: - -
    -
  1. Сначала приводит аргумент к логическому типу `true/false`.
  2. -
  3. Затем возвращает противоположное значение.
  4. -
- -Например: - -```js -//+ run -alert( !true ); // false -alert( !0 ); // true -``` - -**В частности, двойное НЕ используют для преобразования значений к логическому типу:** - -```js -//+ run -alert( !!"строка" ); // true -alert( !!null ); // false -``` - diff --git a/1-js/2-first-steps/14-types-conversion/1-primitive-conversions-questions/solution.md b/1-js/2-first-steps/14-types-conversion/1-primitive-conversions-questions/solution.md deleted file mode 100644 index 7a0cbf25..00000000 --- a/1-js/2-first-steps/14-types-conversion/1-primitive-conversions-questions/solution.md +++ /dev/null @@ -1,34 +0,0 @@ - - -```js -//+ no-beautify -"" + 1 + 0 = "10" // (1) -"" - 1 + 0 = -1 // (2) -true + false = 1 -6 / "3" = 2 -"2" * "3" = 6 -4 + 5 + "px" = "9px" -"$" + 4 + 5
 = "$45" -"4" - 2
 = 2 -"4px" - 2
 = NaN -7 / 0
 = Infinity -" -9\n" + 5 = " -9\n5" -" -9\n" - 5 = -14 -5 && 2
 = 2 -2 && 5
 = 5 -5 || 0
 = 5 -0 || 5 = 5 -null + 1 = 1 // (3) -undefined + 1 = NaN // (4) -null == "\n0\n" = false // (5) -+null == +"\n0\n" = true // (6) -``` - -
    -
  1. Оператор `"+"` в данном случае прибавляет `1` как строку, и затем `0`.
  2. -
  3. Оператор `"-"` работает только с числами, так что он сразу приводит `""` к `0`.
  4. -
  5. `null` при численном преобразовании становится `0`
  6. -
  7. `undefined` при численном преобразовании становится `NaN`
  8. -
  9. При сравнении `==` с `null` преобразования не происходит, есть жёсткое правило: `null == undefined` и только.
  10. -
  11. И левая и правая часть `==` преобразуются к числу `0`.
  12. -
\ No newline at end of file diff --git a/1-js/2-first-steps/14-types-conversion/1-primitive-conversions-questions/task.md b/1-js/2-first-steps/14-types-conversion/1-primitive-conversions-questions/task.md deleted file mode 100644 index 1c2c953f..00000000 --- a/1-js/2-first-steps/14-types-conversion/1-primitive-conversions-questions/task.md +++ /dev/null @@ -1,30 +0,0 @@ -# Вопросник по преобразованиям, для примитивов - -[importance 5] - -Подумайте, какой результат будет у выражений ниже. Тут не только преобразования типов. Когда закончите -- сверьтесь с решением. - -```js -//+ no-beautify -"" + 1 + 0 -"" - 1 + 0 -true + false -6 / "3" -"2" * "3" -4 + 5 + "px" -"$" + 4 + 5
 -"4" - 2
 -"4px" - 2
 -7 / 0
 -" -9\n" + 5 -" -9\n" - 5 -5 && 2
 -2 && 5
 -5 || 0
 -0 || 5 -null + 1 -undefined + 1 -null == "\n0\n" -+null == +"\n0\n" -``` - diff --git a/1-js/2-first-steps/14-types-conversion/article.md b/1-js/2-first-steps/14-types-conversion/article.md deleted file mode 100644 index 3c7d44ab..00000000 --- a/1-js/2-first-steps/14-types-conversion/article.md +++ /dev/null @@ -1,212 +0,0 @@ -# Преобразование типов для примитивов - -Система преобразования типов в JavaScript очень проста, но отличается от других языков. Поэтому она часто служит "камнем преткновения" для приходящих из других языков программистов. -[cut] -Всего есть три преобразования: -
    -
  1. Cтроковое преобразование.
  2. -
  3. Числовое преобразование.
  4. -
  5. Преобразование к логическому значению.
  6. -
- -**Эта глава описывает преобразование только примитивных значений, объекты разбираются далее.** - - -## Строковое преобразование - -Строковое преобразование происходит, когда требуется представление чего-либо в виде строки. Например, его производит функция `alert`. - -```js -//+ run -var a = true; - -alert( a ); // "true" -``` - -Можно также осуществить преобразование явным вызовом `String(val)`: - -```js -//+ run -alert( String(null) === "null" ); // true -``` - -Как видно из примеров выше, преобразование происходит наиболее очевидным способом, "как есть": `false` становится `"false"`, `null` -- `"null"`, `undefined` -- `"undefined"` и т.п. - -Также для явного преобразования применяется оператор `"+"`, у которого один из аргументов строка. В этом случае он приводит к строке и другой аргумент, например: - -```js -//+ run -alert( true + "test" ); // "truetest" -alert( "123" + undefined ); // "123undefined" -``` - -## Численное преобразование - -Численное преобразование происходит в математических функциях и выражениях, а также при сравнении данных различных типов (кроме сравнений `===`, `!==`). - -Для преобразования к числу в явном виде можно вызвать `Number(val)`, либо, что короче, поставить перед выражением унарный плюс `"+"`: - -```js -var a = +"123"; // 123 -var a = Number("123"); // 123, тот же эффект -``` - - - - - - - - - - - -
ЗначениеПреобразуется в...
`undefined``NaN`
`null``0`
`true / false``1 / 0`
СтрокаПробельные символы по краям обрезаются.
Далее, если остаётся пустая строка, то `0`, иначе из непустой строки "считывается" число, при ошибке результат `NaN`.
- -Например: - -```js -//+ run -// после обрезания пробельных символов останется "123" -alert( +" \n 123 \n \n" ); // 123 -``` - -Ещё примеры: -
    -
  • Логические значения: - -```js -//+ run -alert( +true ); // 1 -alert( +false ); // 0 -``` - -
  • -
  • Сравнение разных типов -- значит численное преобразование: - -```js -//+ run -alert( "\n0 " == 0 ); // true -``` - -При этом строка `"\n0"` преобразуется к числу, как указано выше: начальные и конечные пробелы обрезаются, получается строка `"0"`, которая равна `0`.
  • - -
  • С логическими значениями: - -```js -//+ run -alert( "\n" == false ); -alert( "1" == true ); -``` - -Здесь сравнение `"=="` снова приводит обе части к числу. В первой строке слева и справа получается `0`, во второй `1`. -
  • -
- -### Специальные значения - -Посмотрим на поведение специальных значений более внимательно. - -**Интуитивно, значения `null/undefined` ассоциируются с нулём, но при преобразованиях ведут себя иначе.** - -Специальные значения преобразуются к числу так: - - - - -
ЗначениеПреобразуется в...
`undefined``NaN`
`null``0`
- -Это преобразование осуществляется при арифметических операциях и сравнениях `> >= < <=`, но не при проверке равенства `==`. Алгоритм проверки равенства для этих значений в спецификации прописан отдельно (пункт [11.9.3](http://es5.github.com/x11.html#x11.9.3)). В нём считается, что `null` и `undefined` равны `"=="` между собой, но эти значения не равны никакому другому значению. - -Это ведёт к забавным последствиям. - -Например, `null` не подчиняется законам математики -- он "больше либо равен нулю": `null>=0`, но не больше и не равен: - -```js -//+ run -alert( null >= 0 ); // true, т.к. null преобразуется к 0 -alert( null > 0 ); // false (не больше), т.к. null преобразуется к 0 -alert( null == 0 ); // false (и не равен!), т.к. == рассматривает null особо. -``` - -Значение `undefined` вообще "несравнимо": - -```js -//+ run -alert( undefined > 0 ); // false, т.к. undefined -> NaN -alert( undefined == 0 ); // false, т.к. это undefined (без преобразования) -alert( undefined < 0 ); // false, т.к. undefined -> NaN -``` - -**Для более очевидной работы кода и во избежание ошибок лучше не давать специальным значениям участвовать в сравнениях `> >= < <=`.** - -Используйте в таких случаях переменные-числа или приводите к числу явно. - -## Логическое преобразование - -Преобразование к `true/false` происходит в логическом контексте, таком как `if(value)`, и при применении логических операторов. - -Все значения, которые интуитивно "пусты", становятся `false`. Их несколько: `0`, пустая строка, `null`, `undefined` и `NaN`. - -Остальное, в том числе и любые объекты -- `true`. - -Полная таблица преобразований: - - - - - - - -
ЗначениеПреобразуется в...
`undefined`, `null``false`
ЧислаВсе `true`, кроме `0`, `NaN` -- `false`.
СтрокиВсе `true`, кроме пустой строки `""` -- `false`
ОбъектыВсегда `true`
- -**Для явного преобразования используется двойное логическое отрицание `!!value` или вызов `Boolean(value)`.** - -[warn header="Обратите внимание: строка `\"0\"` становится `true`"] -В отличие от многих языков программирования (например PHP), `"0"` в JavaScript является `true`, как и строка из пробелов: - -```js -//+ run -alert( !!"0" ); // true -alert( !!" " ); // любые непустые строки, даже из пробелов - true! -``` - -[/warn] - - -Логическое преобразование интересно тем, как оно сочетается с численным. - -**Два значения могут быть равны, но одно из них в логическом контексте `true`, другое -- `false`**. - -Например, равенство в следующем примере верно, так как происходит численное преобразование: - -```js -//+ run -alert( 0 == "\n0\n" ); // true -``` - -...А в логическом контексте левая часть даст `false`, правая -- `true`: - -```js -//+ run -if ("\n0\n") { - alert( "true, совсем не как 0!" ); -} -``` - -С точки зрения преобразования типов в JavaScript это совершенно нормально. При равенстве -- численное преобразование, а в `if` -- логическое, только и всего. - -## Итого - -В JavaScript есть три преобразования: - -
    -
  1. Строковое: `String(value)` -- в строковом контексте или при сложении со строкой. Работает очевидным образом.
  2. -
  3. Численное: `Number(value)` -- в численном контексте, включая унарный плюс `+value`. Происходит при сравнении разных типов, кроме строгого равенства.
  4. -
  5. Логическое: `Boolean(value)` -- в логическом контексте, можно также сделать двойным НЕ: `!!value`.
  6. -
- -Точные таблицы преобразований даны выше в этой главе. - -Особым случаем является проверка равенства с `null` и `undefined`. Они равны друг другу, но не равны чему бы то ни было ещё, этот случай прописан особо в спецификации. - diff --git a/1-js/2-first-steps/15-while-for/1-loop-last-value/solution.md b/1-js/2-first-steps/15-while-for/1-loop-last-value/solution.md deleted file mode 100644 index 15c634c9..00000000 --- a/1-js/2-first-steps/15-while-for/1-loop-last-value/solution.md +++ /dev/null @@ -1,26 +0,0 @@ -Ответ: `1`. - -```js -//+ run -var i = 3; - -while (i) { - alert( i-- ); -} -``` - -Каждое выполнение цикла уменьшает `i`. Проверка `while(i)` даст сигнал "стоп" при `i = 0`. - -Соответственно, шаги цикла: - -```js -var i = 3 -alert( i-- ); // выведет 3, затем уменьшит i до 2 - -alert(i--) // выведет 2, затем уменьшит i до 1 - -alert(i--) // выведет 1, затем уменьшит i до 0 - -// все, проверка while(i) не даст выполняться циклу дальше -``` - diff --git a/1-js/2-first-steps/15-while-for/1-loop-last-value/task.md b/1-js/2-first-steps/15-while-for/1-loop-last-value/task.md deleted file mode 100644 index 25359633..00000000 --- a/1-js/2-first-steps/15-while-for/1-loop-last-value/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Последнее значение цикла - -[importance 3] - -Какое последнее значение выведет этот код? Почему? - -```js -var i = 3; - -while (i) { - alert( i-- ); -} -``` - diff --git a/1-js/2-first-steps/15-while-for/2-which-value-while/solution.md b/1-js/2-first-steps/15-while-for/2-which-value-while/solution.md deleted file mode 100644 index f7e77582..00000000 --- a/1-js/2-first-steps/15-while-for/2-which-value-while/solution.md +++ /dev/null @@ -1,31 +0,0 @@ -
    -
  1. **От 1 до 4** - -```js -//+ run -var i = 0; -while (++i < 5) alert( i ); -``` - -Первое значение: `i=1`, так как операция `++i` сначала увеличит `i`, а потом уже произойдёт сравнение и выполнение `alert`. - -Далее `2,3,4..` Значения выводятся одно за другим. Для каждого значения сначала происходит увеличение, а потом -- сравнение, так как `++` стоит перед переменной. - -При `i=4` произойдет увеличение `i` до `5`, а потом сравнение `while(5 < 5)` -- это неверно. Поэтому на этом цикл остановится, и значение `5` выведено не будет. -
  2. -
  3. **От 1 до 5** - -```js -//+ run -var i = 0; -while (i++ < 5) alert( i ); -``` - -Первое значение: `i=1`. Остановимся на нём подробнее. Оператор `i++` увеличивает `i`, возвращая старое значение, так что в сравнении `i++ < 5` будет участвовать старое `i=0`. - -Но последующий вызов `alert` уже не относится к этому выражению, так что получит новый `i=1`. - -Далее `2,3,4..` Для каждого значения сначала происходит сравнение, а потом -- увеличение, и затем срабатывание `alert`. - -Окончание цикла: при `i=4` произойдет сравнение `while(4 < 5)` -- верно, после этого сработает `i++`, увеличив `i` до `5`, так что значение `5` будет выведено. Оно станет последним.
  4. -
\ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/2-which-value-while/task.md b/1-js/2-first-steps/15-while-for/2-which-value-while/task.md deleted file mode 100644 index 674f005b..00000000 --- a/1-js/2-first-steps/15-while-for/2-which-value-while/task.md +++ /dev/null @@ -1,23 +0,0 @@ -# Какие значения i выведет цикл while? - -[importance 4] - -Для каждого цикла запишите, какие значения он выведет. Потом сравните с ответом. -
    -
  1. Префиксный вариант - -```js -var i = 0; -while (++i < 5) alert( i ); -``` - -
  2. -
  3. Постфиксный вариант - -```js -var i = 0; -while (i++ < 5) alert( i ); -``` - -
  4. -
\ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/3-which-value-for/solution.md b/1-js/2-first-steps/15-while-for/3-which-value-for/solution.md deleted file mode 100644 index 941a07be..00000000 --- a/1-js/2-first-steps/15-while-for/3-which-value-for/solution.md +++ /dev/null @@ -1,17 +0,0 @@ -**Ответ: от 0 до 4 в обоих случаях.** - -```js -//+ run -for (var i = 0; i < 5; ++i) alert( i ); - -for (var i = 0; i < 5; i++) alert( i ); -``` - -Такой результат обусловлен алгоритмом работы `for`: -
    -
  1. Выполнить присвоение `i=0`
  2. -
  3. Проверить `i<5`
  4. -
  5. Если верно - выполнить тело цикла `alert(i)`, затем выполнить `i++`
  6. -
- -Увеличение `i++` выполняется отдельно от проверки условия (2), значение `i` при этом не используется, поэтому нет никакой разницы между `i++` и `++i`. \ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/3-which-value-for/task.md b/1-js/2-first-steps/15-while-for/3-which-value-for/task.md deleted file mode 100644 index e918c89c..00000000 --- a/1-js/2-first-steps/15-while-for/3-which-value-for/task.md +++ /dev/null @@ -1,21 +0,0 @@ -# Какие значения i выведет цикл for? - -[importance 4] - -Для каждого цикла запишите, какие значения он выведет. Потом сравните с ответом. -
    -
  1. Постфиксная форма: - -```js -for (var i = 0; i < 5; i++) alert( i ); -``` - -
  2. -
  3. Префиксная форма: - -```js -for (var i = 0; i < 5; ++i) alert( i ); -``` - -
  4. -
\ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/4-for-even/solution.md b/1-js/2-first-steps/15-while-for/4-for-even/solution.md deleted file mode 100644 index 62ca05c7..00000000 --- a/1-js/2-first-steps/15-while-for/4-for-even/solution.md +++ /dev/null @@ -1,12 +0,0 @@ - - -```js -//+ run demo -for (var i = 2; i <= 10; i++) { - if (i % 2 == 0) { - alert( i ); - } -} -``` - -Чётность проверяется по остатку при делении на `2`, используя оператор "деление с остатком" `%`: `i % 2`. \ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/4-for-even/task.md b/1-js/2-first-steps/15-while-for/4-for-even/task.md deleted file mode 100644 index d20029d0..00000000 --- a/1-js/2-first-steps/15-while-for/4-for-even/task.md +++ /dev/null @@ -1,7 +0,0 @@ -# Выведите чётные числа - -[importance 5] - -При помощи цикла `for` выведите чётные числа от `2` до `10`. - -[demo /] \ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/5-replace-for-while/solution.md b/1-js/2-first-steps/15-while-for/5-replace-for-while/solution.md deleted file mode 100644 index 84915ab4..00000000 --- a/1-js/2-first-steps/15-while-for/5-replace-for-while/solution.md +++ /dev/null @@ -1,11 +0,0 @@ - - -```js -//+ run -var i = 0; -while (i < 3) { - alert( "номер " + i + "!" ); - i++; -} -``` - diff --git a/1-js/2-first-steps/15-while-for/5-replace-for-while/task.md b/1-js/2-first-steps/15-while-for/5-replace-for-while/task.md deleted file mode 100644 index 2da6d6f2..00000000 --- a/1-js/2-first-steps/15-while-for/5-replace-for-while/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Замените for на while - -[importance 5] - -Перепишите код, заменив цикл `for` на `while`, без изменения поведения цикла. - -```js -//+ run -for (var i = 0; i < 3; i++) { - alert( "номер " + i + "!" ); -} -``` - diff --git a/1-js/2-first-steps/15-while-for/6-repeat-until-correct/solution.md b/1-js/2-first-steps/15-while-for/6-repeat-until-correct/solution.md deleted file mode 100644 index 49d5396e..00000000 --- a/1-js/2-first-steps/15-while-for/6-repeat-until-correct/solution.md +++ /dev/null @@ -1,18 +0,0 @@ - - -```js -//+ run demo -var num; - -do { - num = prompt("Введите число больше 100?", 0); -} while (num <= 100 && num != null); -``` - -Цикл `do..while` повторяется, пока верны две проверки: -
    -
  1. Проверка `num <= 100` -- то есть, введённое число всё еще меньше `100`.
  2. -
  3. Проверка `num != null` -- значение `null` означает, что посетитель нажал "Отмена", в этом случае цикл тоже нужно прекратить.
  4. -
- -Кстати, сравнение `num <= 100` при вводе `null` даст `true`, так что вторая проверка необходима. \ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/6-repeat-until-correct/task.md b/1-js/2-first-steps/15-while-for/6-repeat-until-correct/task.md deleted file mode 100644 index 4c2fbedc..00000000 --- a/1-js/2-first-steps/15-while-for/6-repeat-until-correct/task.md +++ /dev/null @@ -1,11 +0,0 @@ -# Повторять цикл, пока ввод неверен - -[importance 5] - -Напишите цикл, который предлагает `prompt` ввести число, большее `100`. Если посетитель ввёл другое число -- попросить ввести ещё раз, и так далее. - -Цикл должен спрашивать число пока либо посетитель не введёт число, большее `100`, либо не нажмёт кнопку Cancel (ESC). - -Предполагается, что посетитель вводит только числа. Предусматривать обработку нечисловых строк в этой задаче необязательно. - -[demo /] diff --git a/1-js/2-first-steps/15-while-for/7-list-primes/solution.md b/1-js/2-first-steps/15-while-for/7-list-primes/solution.md deleted file mode 100644 index eead33a3..00000000 --- a/1-js/2-first-steps/15-while-for/7-list-primes/solution.md +++ /dev/null @@ -1,28 +0,0 @@ -# Схема решения - -```js -Для всех i от 1 до 10 { - проверить, делится ли число i на какое - либо из чисел до него - если делится, то это i не подходит, берем следующее - если не делится, то i - простое число -} -``` - -# Решение - -Решение с использованием метки: - -```js -//+ run -nextPrime: - for (var i = 2; i < 10; i++) { - - for (var j = 2; j < i; j++) { - if (i % j == 0) continue nextPrime; - } - - alert( i ); // простое - } -``` - -Конечно же, его можно оптимизировать с точки зрения производительности. Например, проверять все `j` не от `2` до `i`, а от `2` до квадратного корня из `i`. А для очень больших чисел -- существуют более эффективные специализированные алгоритмы проверки простоты числа, например [квадратичное решето](http://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D1%82%D0%BE%D0%B4_%D0%BA%D0%B2%D0%B0%D0%B4%D1%80%D0%B0%D1%82%D0%B8%D1%87%D0%BD%D0%BE%D0%B3%D0%BE_%D1%80%D0%B5%D1%88%D0%B5%D1%82%D0%B0) и [решето числового поля](http://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%89%D0%B8%D0%B9_%D0%BC%D0%B5%D1%82%D0%BE%D0%B4_%D1%80%D0%B5%D1%88%D0%B5%D1%82%D0%B0_%D1%87%D0%B8%D1%81%D0%BB%D0%BE%D0%B2%D0%BE%D0%B3%D0%BE_%D0%BF%D0%BE%D0%BB%D1%8F). \ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/7-list-primes/task.md b/1-js/2-first-steps/15-while-for/7-list-primes/task.md deleted file mode 100644 index 51f8c29f..00000000 --- a/1-js/2-first-steps/15-while-for/7-list-primes/task.md +++ /dev/null @@ -1,12 +0,0 @@ -# Вывести простые числа - -[importance 3] - -Натуральное число, большее 1, называется *простым*, если оно ни на что не делится, кроме себя и `1`. - -Другими словами, n>1 - простое, если при делении на любое число от `2` до `n-1` есть остаток. - -**Создайте код, который выводит все простые числа из интервала от `2` до `10`.** Результат должен быть: `2,3,5,7`. - -P.S. Код также должен легко модифицироваться для любых других интервалов. - diff --git a/1-js/2-first-steps/15-while-for/article.md b/1-js/2-first-steps/15-while-for/article.md deleted file mode 100644 index 71991707..00000000 --- a/1-js/2-first-steps/15-while-for/article.md +++ /dev/null @@ -1,349 +0,0 @@ -# Циклы while, for - -При написании скриптов зачастую встает задача сделать однотипное действие много раз. - -Например, вывести товары из списка один за другим. Или просто перебрать все числа от 1 до 10 и для каждого выполнить одинаковый код. - -Для многократного повторения одного участка кода - предусмотрены *циклы*. -[cut] -## Цикл while - -Цикл `while` имеет вид: - -```js -while (условие) { - // код, тело цикла -} -``` - -Пока `условие` верно -- выполняется код из тела цикла. - -Например, цикл ниже выводит `i` пока `i < 3`: - -```js -//+ run -var i = 0; -while (i < 3) { - alert( i ); - i++; -} -``` - -**Повторение цикла по-научному называется *"итерация"*. Цикл в примере выше совершает три итерации.** - -Если бы `i++` в коде выше не было, то цикл выполнялся бы (в теории) вечно. На практике, браузер выведет сообщение о "зависшем" скрипте и посетитель его остановит. - -**Бесконечный цикл** можно сделать и проще: - -```js -while (true) { - // ... -} -``` - -**Условие в скобках интерпретируется как логическое значение, поэтому вместо `while (i!=0)` обычно пишут `while (i)`**: - -```js -//+ run -var i = 3; -*!* -while (i) { // при i, равном 0, значение в скобках будет false и цикл остановится -*/!* - alert( i ); - i--; -} -``` - -## Цикл do..while - -Проверку условия можно поставить *под* телом цикла, используя специальный синтаксис `do..while`: - -```js -do { - // тело цикла -} while (условие); -``` - -Цикл, описанный, таким образом, сначала выполняет тело, а затем проверяет условие. - -Например: - -```js -//+ run -var i = 0; -do { - alert( i ); - i++; -} while (i < 3); -``` - -Синтаксис `do..while` редко используется, т.к. обычный `while` нагляднее -- в нём не приходится искать глазами условие и ломать голову, почему оно проверяется именно в конце. - - -## Цикл for - -Чаще всего применяется цикл `for`. Выглядит он так: - -```js -for (начало; условие; шаг) { - // ... тело цикла ... -} -``` - -Пример цикла, который выполняет `alert(i)` для `i` от `0` до `2` включительно (до `3`): - -```js -//+ run -var i; - -for (i = 0; i < 3; i++) { - alert( i ); -} -``` - -Здесь: -
    -
  • **Начало:** `i=0`.
  • -
  • **Условие:** `i<3`.
  • -
  • **Шаг:** `i++`.
  • -
  • **Тело:** `alert(i)`, т.е. код внутри фигурных скобок (они не обязательны, если только одна операция)
  • -
- -Цикл выполняется так: - -
    -
  1. Начало: `i=0` выполняется один-единственный раз, при заходе в цикл.
  2. -
  3. Условие: `i<3` проверяется перед каждой итерацией и при входе в цикл, если оно нарушено, то происходит выход.
  4. -
  5. Тело: `alert(i)`.
  6. -
  7. Шаг: `i++` выполняется после *тела* на каждой итерации, но перед проверкой условия.
  8. -
  9. Идти на шаг 2.
  10. -
- -Иными словами, поток выполнения: `начало` -> (если `условие` -> `тело` -> `шаг`) -> (если `условие` -> `тело` -> `шаг`) -> ... и так далее, пока верно `условие`. - -[smart] -В цикле также можно определить переменную: - -```js -//+ run no-beautify -for (*!*var*/!* i = 0; i < 3; i++) { - alert(i); // 0, 1, 2 -} -``` - -Эта переменная будет видна и за границами цикла, в частности, после окончания цикла `i` станет равно `3`. -[/smart] - -## Пропуск частей for - -Любая часть `for` может быть пропущена. - -Например, можно убрать `начало`. Цикл в примере ниже полностью идентичен приведённому выше: - -```js -//+ run -var i = 0; - -for (; i < 3; i++) { - alert( i ); // 0, 1, 2 -} -``` - -Можно убрать и `шаг`: - -```js -//+ run -var i = 0; - -for (; i < 3;) { - alert( i ); - // цикл превратился в аналог while (i<3) -} -``` - -А можно и вообще убрать всё, получив бесконечный цикл: - -```js -for (;;) { - // будет выполняться вечно -} -``` - -При этом сами точки с запятой `;` обязательно должны присутствовать, иначе будет ошибка синтаксиса. - -[smart header="`for..in`"] -Существует также специальная конструкция `for..in` для перебора свойств объекта. - -Мы познакомимся с ней позже, когда будем [говорить об объектах](#for..in). -[/smart] - - -## Прерывание цикла: break - -Выйти из цикла можно не только при проверке условия но и, вообще, в любой момент. Эту возможность обеспечивает директива `break`. - -Например, следующий код подсчитывает сумму вводимых чисел до тех пор, пока посетитель их вводит, а затем -- выдаёт: - -```js -var sum = 0; - -while (true) { - - var value = +prompt("Введите число", ''); - -*!* - if (!value) break; // (*) -*/!* - - sum += value; - -} -alert( 'Сумма: ' + sum ); -``` - -Директива `break` в строке `(*)`, если посетитель ничего не ввёл, полностью прекращает выполнение цикла и передаёт управление на строку за его телом, то есть на `alert`. - -Вообще, сочетание "бесконечный цикл + break" -- отличная штука для тех ситуаций, когда условие, по которому нужно прерваться, находится не в начале-конце цикла, а посередине. - -## Следующая итерация: continue [#continue] - -Директива `continue` прекращает выполнение *текущей итерации* цикла. - -Она -- в некотором роде "младшая сестра" директивы `break`: прерывает не весь цикл, а только текущее выполнение его тела, как будто оно закончилось. - -Её используют, если понятно, что на текущем повторе цикла делать больше нечего. - -Например, цикл ниже использует `continue`, чтобы не выводить чётные значения: - -```js -//+ run no-beautify -for (var i = 0; i < 10; i++) { - - *!*if (i % 2 == 0) continue;*/!* - - alert(i); -} -``` - -Для чётных `i` срабатывает `continue`, выполнение тела прекращается и управление передаётся на следующий проход `for`. - -[smart header="Директива `continue` позволяет обойтись без скобок"] - -Цикл, который обрабатывает только нечётные значения, мог бы выглядеть так: - -```js -for (var i = 0; i < 10; i++) { - - if (i % 2) { - alert( i ); - } - -} -``` - -С технической точки зрения он полностью идентичен. Действительно, вместо `continue` можно просто завернуть действия в блок `if`. Однако, мы получили дополнительный уровень вложенности фигурных скобок. Если код внутри `if` более длинный, то это ухудшает читаемость, в отличие от варианта с `continue`. -[/smart] - -[warn header="Нельзя использовать break/continue справа от оператора '?'"] -Обычно мы можем заменить `if` на оператор вопросительный знак `'?'`. - -То есть, запись: - -```js -if (условие) { - a(); -} else { - b(); -} -``` - -...Аналогична записи: - -```js -условие ? a() : b(); -``` - -В обоих случаях в зависимости от условия выполняется либо `a()` либо `b()`. - -Но разница состоит в том, что оператор вопросительный знак `'?'`, использованный во второй записи, возвращает значение. - -**Синтаксические конструкции, которые не возвращают значений, нельзя использовать в операторе `'?'`.** - -К таким относятся большинство конструкций и, в частности, `break/continue`. - -Поэтому такой код приведёт к ошибке: - -```js -//+ no-beautify -(i > 5) ? alert(i) : *!*continue*/!*; -``` - -Впрочем, как уже говорилось ранее, оператор вопросительный знак `'?'` не стоит использовать таким образом. Это -- всего лишь ещё одна причина, почему для проверки условия предпочтителен `if`. -[/warn] - -## Метки для break/continue - -Бывает нужно выйти одновременно из нескольких уровней цикла. - -Например, внутри цикла по `i` находится цикл по `j`, и при выполнении некоторого условия мы бы хотели выйти из обоих циклов сразу: - -```js -//+ run no-beautify -*!*outer:*/!* for (var i = 0; i < 3; i++) { - - for (var j = 0; j < 3; j++) { - - var input = prompt('Значение в координатах '+i+','+j, ''); - - // если отмена ввода или пустая строка - - // завершить оба цикла - if (!input) *!*break outer*/!*; // (*) - - } -} -alert('Готово!'); -``` - -В коде выше для этого использована *метка*. - -Метка имеет вид `"имя:"`, имя должно быть уникальным. Она ставится перед циклом, вот так: - -```js -//+ no-beautify -outer: for (var i = 0; i < 3; i++) { ... } -``` - -Можно также выносить её на отдельную строку: - -```js -//+ no-beautify -outer: -for (var i = 0; i < 3; i++) { ... } -``` - -Вызов `break outer` ищет ближайший внешний цикл с такой меткой и переходит в его конец. - -В примере выше это означает, что будет разорван самый внешний цикл и управление перейдёт на `alert`. - -Директива `continue` также может быть использована с меткой, в этом случае управление перепрыгнет на следующую итерацию цикла с меткой. - -## Итого - -JavaScript поддерживает три вида циклов: -
    -
  • `while` -- проверка условия перед каждым выполнением.
  • -
  • `do..while` -- проверка условия после каждого выполнения.
  • -
  • `for` -- проверка условия перед каждым выполнением, а также дополнительные настройки.
  • -
- -Чтобы организовать бесконечный цикл, используют конструкцию `while(true)`. При этом он, как и любой другой цикл, может быть прерван директивой `break`. - -Если на данной итерации цикла делать больше ничего не надо, но полностью прекращать цикл не следует -- используют директиву `continue`. - -Обе этих директивы поддерживают "метки", которые ставятся перед циклом. Метки -- единственный способ для `break/continue` повлиять на выполнение внешнего цикла. - -Заметим, что метки не позволяют прыгнуть в произвольное место кода, в JavaScript нет такой возможности. - - - - diff --git a/1-js/2-first-steps/16-switch/1-rewrite-switch-if-else/solution.md b/1-js/2-first-steps/16-switch/1-rewrite-switch-if-else/solution.md deleted file mode 100644 index 1605333f..00000000 --- a/1-js/2-first-steps/16-switch/1-rewrite-switch-if-else/solution.md +++ /dev/null @@ -1,21 +0,0 @@ -Если совсем точно следовать условию, то сравнение должно быть строгим `'==='`. - -В реальном случае, скорее всего, подойдёт обычное сравнение `'=='`. - -```js -//+ no-beautify -if(browser == 'IE') { - alert('О, да у вас IE!'); -} else if (browser == 'Chrome' - || browser == 'Firefox' - || browser == 'Safari' - || browser == 'Opera') { - alert('Да, и эти браузеры мы поддерживаем'); -} else { - alert('Мы надеемся, что и в вашем браузере все ок!'); -} -``` - -Обратите внимание: конструкция `browser == 'Chrome' || browser == 'Firefox' ...` разбита на несколько строк для лучшей читаемости. - -Но всё равно запись через `switch` нагляднее. \ No newline at end of file diff --git a/1-js/2-first-steps/16-switch/1-rewrite-switch-if-else/task.md b/1-js/2-first-steps/16-switch/1-rewrite-switch-if-else/task.md deleted file mode 100644 index 15bf197e..00000000 --- a/1-js/2-first-steps/16-switch/1-rewrite-switch-if-else/task.md +++ /dev/null @@ -1,24 +0,0 @@ -# Напишите "if", аналогичный "switch" - -[importance 5] - -Напишите `if..else`, соответствующий следующему `switch`: - -```js -switch (browser) { - case 'IE': - alert( 'О, да у вас IE!' ); - break; - - case 'Chrome': - case 'Firefox': - case 'Safari': - case 'Opera': - alert( 'Да, и эти браузеры мы поддерживаем' ); - break; - - default: - alert( 'Мы надеемся, что и в вашем браузере все ок!' ); -} -``` - diff --git a/1-js/2-first-steps/16-switch/2-rewrite-if-switch/solution.md b/1-js/2-first-steps/16-switch/2-rewrite-if-switch/solution.md deleted file mode 100644 index ab738f53..00000000 --- a/1-js/2-first-steps/16-switch/2-rewrite-if-switch/solution.md +++ /dev/null @@ -1,27 +0,0 @@ -Первые две проверки -- обычный `case`, третья разделена на два `case`: - -```js -//+ run -var a = +prompt('a?', ''); - -switch (a) { - case 0: - alert( 0 ); - break; - - case 1: - alert( 1 ); - break; - - case 2: - case 3: - alert( '2,3' ); -*!* - break; -*/!* -} -``` - -Обратите внимание: `break` внизу не обязателен, но ставится по "правилам хорошего тона". - -Допустим, он не стоит. Есть шанс, что в будущем нам понадобится добавить в конец ещё один `case`, например `case 4`, и мы, вполне вероятно, забудем этот `break` поставить. В результате выполнение `case 2`/`case 3` продолжится на `case 4` и будет ошибка. diff --git a/1-js/2-first-steps/16-switch/2-rewrite-if-switch/task.md b/1-js/2-first-steps/16-switch/2-rewrite-if-switch/task.md deleted file mode 100644 index a542c993..00000000 --- a/1-js/2-first-steps/16-switch/2-rewrite-if-switch/task.md +++ /dev/null @@ -1,22 +0,0 @@ -# Переписать if'ы в switch - -[importance 4] - -Перепишите код с использованием одной конструкции `switch`: - -```js -//+ run -var a = +prompt('a?', ''); - -if (a == 0) { - alert( 0 ); -} -if (a == 1) { - alert( 1 ); -} - -if (a == 2 || a == 3) { - alert( '2,3' ); -} -``` - diff --git a/1-js/2-first-steps/16-switch/article.md b/1-js/2-first-steps/16-switch/article.md deleted file mode 100644 index 9a1f8fff..00000000 --- a/1-js/2-first-steps/16-switch/article.md +++ /dev/null @@ -1,186 +0,0 @@ -# Конструкция switch - -Конструкция `switch` заменяет собой сразу несколько `if`. - -Она представляет собой более наглядный способ сравнить выражение сразу с несколькими вариантами. -[cut] -## Синтаксис - -Выглядит она так: - -```js -//+ no-beautify -switch(x) { - case 'value1': // if (x === 'value1') - ... - [break] - - case 'value2': // if (x === 'value2') - ... - [break] - - default: - ... - [break] -} -``` - -
    -
  • -Переменная `x` проверяется на строгое равенство первому значению `value1`, затем второму `value2` и так далее. -
  • -
  • -Если соответствие установлено -- switch начинает выполняться от соответствующей директивы `case` и далее, *до ближайшего `break`* (или до конца `switch`). -
  • -
  • -Если ни один `case` не совпал -- выполняетcя (если есть) вариант `default`. -
  • -
- -При этом `case` называют *вариантами `switch`*. - -## Пример работы - -Пример использования `switch` (сработавший код выделен): - -```js -//+ run -var a = 2 + 2; - -switch (a) { - case 3: - alert( 'Маловато' ); - break; -*!* - case 4: - alert( 'В точку!' ); - break; -*/!* - case 5: - alert( 'Перебор' ); - break; - default: - alert( 'Я таких значений не знаю' ); -} -``` - -Здесь оператор `switch` последовательно сравнит `a` со всеми вариантами из `case`. - -Сначала `3`, затем -- так как нет совпадения -- `4`. Совпадение найдено, будет выполнен этот вариант, со строки `alert('В точку!')` и далее, до ближайшего `break`, который прервёт выполнение. - -**Если `break` нет, то выполнение пойдёт ниже по следующим `case`, при этом остальные проверки игнорируются.** - -Пример без `break`: - -```js -//+ run -var a = 2 + 2; - -switch (a) { - case 3: - alert( 'Маловато' ); -*!* - case 4: - alert( 'В точку!' ); - case 5: - alert( 'Перебор' ); - default: - alert( 'Я таких значений не знаю' ); -*/!* -} -``` - -В примере выше последовательно выполнятся три `alert`: - -```js -alert( 'В точку!' ); -alert( 'Перебор' ); -alert( 'Я таких значений не знаю' ); -``` - -В `case` могут быть любые выражения, в том числе включающие в себя переменные и функции. - -Например: - -```js -//+ run -var a = 1; -var b = 0; - -switch (a) { -*!* - case b + 1: - alert( 1 ); - break; -*/!* - - default: - alert('нет-нет, выполнится вариант выше') -} -``` - -## Группировка case - -Несколько значений case можно группировать. - -В примере ниже `case 3` и `case 5` выполняют один и тот же код: - -```js -//+ run no-beautify -var a = 2+2; - -switch (a) { - case 4: - alert('Верно!'); - break; - -*!* - case 3: // (*) - case 5: // (**) - alert('Неверно!'); - alert('Немного ошиблись, бывает.'); - break; -*/!* - - default: - alert('Странный результат, очень странный'); -} -``` - -При `case 3` выполнение идёт со строки `(*)`, при `case 5` -- со строки `(**)`. - -## Тип имеет значение - -Следующий пример принимает значение от посетителя. - -```js -//+ run -var arg = prompt("Введите arg?") -switch (arg) { - case '0': - case '1': - alert( 'Один или ноль' ); - - case '2': - alert( 'Два' ); - break; - - case 3: - alert( 'Никогда не выполнится' ); - - default: - alert('Неизвестное значение: ' + arg) -} -``` - -Что оно выведет при вводе числа 0? Числа 1? 2? 3? - -Подумайте, выпишите свои ответы, исходя из текущего понимания работы `switch` и *потом* читайте дальше... - -
    -
  • При вводе `0` выполнится первый `alert`, далее выполнение продолжится вниз до первого `break` и выведет второй `alert('Два')`. Итого, два вывода `alert`.
  • -
  • При вводе `1` произойдёт то же самое.
  • -
  • При вводе `2`, `switch` перейдет к `case '2'`, и сработает единственный `alert('Два')`.
  • -
  • **При вводе `3`, `switch` перейдет на `default`.** Это потому, что `prompt` возвращает строку `'3'`, а не число. Типы разные. Оператор `switch` предполагает строгое равенство `===`, так что совпадения не будет.
  • -
- diff --git a/1-js/2-first-steps/17-function-basics/1-if-else-required/solution.md b/1-js/2-first-steps/17-function-basics/1-if-else-required/solution.md deleted file mode 100644 index be1daacc..00000000 --- a/1-js/2-first-steps/17-function-basics/1-if-else-required/solution.md +++ /dev/null @@ -1 +0,0 @@ -Оба варианта функции работают одинаково, отличий нет. \ No newline at end of file diff --git a/1-js/2-first-steps/17-function-basics/1-if-else-required/task.md b/1-js/2-first-steps/17-function-basics/1-if-else-required/task.md deleted file mode 100644 index f97c3b92..00000000 --- a/1-js/2-first-steps/17-function-basics/1-if-else-required/task.md +++ /dev/null @@ -1,35 +0,0 @@ -# Обязателен ли "else"? - -[importance 4] - -Следующая функция возвращает `true`, если параметр `age` больше `18`. -В ином случае она задаёт вопрос посредством вызова `confirm` и возвращает его результат. - -```js -function checkAge(age) { - if (age > 18) { - return true; -*!* - } else { - // ... - return confirm('Родители разрешили?'); - } -*/!* -} -``` - -Будет ли эта функция работать как-то иначе, если убрать `else`? - -```js -function checkAge(age) { - if (age > 18) { - return true; - } -*!* - // ... - return confirm('Родители разрешили?'); -*/!* -} -``` - -Есть ли хоть одно отличие в поведении этого варианта? diff --git a/1-js/2-first-steps/17-function-basics/2-rewrite-function-question-or/solution.md b/1-js/2-first-steps/17-function-basics/2-rewrite-function-question-or/solution.md deleted file mode 100644 index f021c7e4..00000000 --- a/1-js/2-first-steps/17-function-basics/2-rewrite-function-question-or/solution.md +++ /dev/null @@ -1,16 +0,0 @@ -Используя оператор `'?'`: - -```js -function checkAge(age) { - return (age > 18) ? true : confirm('Родители разрешили?'); -} -``` - -Используя оператор `||` (самый короткий вариант): - -```js -function checkAge(age) { - return (age > 18) || confirm('Родители разрешили?'); -} -``` - diff --git a/1-js/2-first-steps/17-function-basics/2-rewrite-function-question-or/task.md b/1-js/2-first-steps/17-function-basics/2-rewrite-function-question-or/task.md deleted file mode 100644 index 5d30718a..00000000 --- a/1-js/2-first-steps/17-function-basics/2-rewrite-function-question-or/task.md +++ /dev/null @@ -1,23 +0,0 @@ -# Перепишите функцию, используя оператор '?' или '||' - -[importance 4] - -Следующая функция возвращает `true`, если параметр `age` больше `18`. -В ином случае она задаёт вопрос `confirm` и возвращает его результат. - -```js -function checkAge(age) { - if (age > 18) { - return true; - } else { - return confirm('Родители разрешили?'); - } -} -``` - -Перепишите функцию, чтобы она делала то же самое, но без `if`, в одну строку. -Сделайте два варианта функции `checkAge`: -
    -
  1. Используя оператор `'?'`
  2. -
  3. Используя оператор `||`
  4. -
diff --git a/1-js/2-first-steps/17-function-basics/3-min/solution.md b/1-js/2-first-steps/17-function-basics/3-min/solution.md deleted file mode 100644 index 91809daa..00000000 --- a/1-js/2-first-steps/17-function-basics/3-min/solution.md +++ /dev/null @@ -1,21 +0,0 @@ -Вариант решения с использованием `if`: - -```js -function min(a, b) { - if (a < b) { - return a; - } else { - return b; - } -} -``` - -Вариант решения с оператором `'?'`: - -```js -function min(a, b) { - return a < b ? a : b; -} -``` - -P.S. Случай равенства `a == b` здесь отдельно не рассматривается, так как при этом неважно, что возвращать. \ No newline at end of file diff --git a/1-js/2-first-steps/17-function-basics/3-min/task.md b/1-js/2-first-steps/17-function-basics/3-min/task.md deleted file mode 100644 index 8d042382..00000000 --- a/1-js/2-first-steps/17-function-basics/3-min/task.md +++ /dev/null @@ -1,16 +0,0 @@ -# Функция min - -[importance 1] - -Задача "Hello World" для функций :) - -Напишите функцию `min(a,b)`, которая возвращает меньшее из чисел `a,b`. - -Пример вызовов: - -```js -min(2, 5) == 2 -min(3, -1) == -1 -min(1, 1) == 1 -``` - diff --git a/1-js/2-first-steps/17-function-basics/4-pow/solution.md b/1-js/2-first-steps/17-function-basics/4-pow/solution.md deleted file mode 100644 index 30ec3ce4..00000000 --- a/1-js/2-first-steps/17-function-basics/4-pow/solution.md +++ /dev/null @@ -1,34 +0,0 @@ - - -```js -//+ run demo -/** - * Возводит x в степень n (комментарий JSDoc) - * - * @param {number} x число, которое возводится в степень - * @param {number} n степень, должна быть целым числом больше 1 - * - * @return {number} x в степени n - */ -function pow(x, n) { - var result = x; - - for (var i = 1; i < n; i++) { - result *= x; - } - - return result; -} - -var x = prompt("x?", ''); -var n = prompt("n?", ''); - -if (n <= 1) { - alert('Степень ' + n + - 'не поддерживается, введите целую степень, большую 1' - ); -} else { - alert( pow(x, n) ); -} -``` - diff --git a/1-js/2-first-steps/17-function-basics/4-pow/task.md b/1-js/2-first-steps/17-function-basics/4-pow/task.md deleted file mode 100644 index 0160bfcf..00000000 --- a/1-js/2-first-steps/17-function-basics/4-pow/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# Функция pow(x,n) - -[importance 4] - -Напишите функцию `pow(x,n)`, которая возвращает `x` в степени `n`. Иначе говоря, умножает `x` на себя `n` раз и возвращает результат. - -```js -pow(3, 2) = 3 * 3 = 9 -pow(3, 3) = 3 * 3 * 3 = 27 -pow(1, 100) = 1 * 1 * ...*1 = 1 -``` - -Создайте страницу, которая запрашивает `x` и `n`, а затем выводит результат `pow(x,n)`. - -[demo /] - -P.S. В этой задаче функция обязана поддерживать только натуральные значения `n`, т.е. целые от `1` и выше. \ No newline at end of file diff --git a/1-js/2-first-steps/17-function-basics/article.md b/1-js/2-first-steps/17-function-basics/article.md deleted file mode 100644 index a9fa6f16..00000000 --- a/1-js/2-first-steps/17-function-basics/article.md +++ /dev/null @@ -1,435 +0,0 @@ -# Функции - -Зачастую нам надо повторять одно и то же действие во многих частях программы. - -Например, красиво вывести сообщение необходимо при приветствии посетителя, при выходе посетителя с сайта, ещё где-нибудь. - -Чтобы не повторять один и тот же код во многих местах, придуманы функции. Функции являются основными "строительными блоками" программы. -[cut] -Примеры встроенных функций вы уже видели -- это `alert(message)`, `prompt(message, default)` и `confirm(question)`. Но можно создавать и свои. - -## Объявление - -Пример объявления функции: - -```js -function showMessage() { - alert( 'Привет всем присутствующим!' ); -} -``` - -Вначале идет ключевое слово `function`, после него *имя функции*, затем *список параметров* в скобках (в примере выше он пустой) и *тело функции* -- код, который выполняется при её вызове. - -Объявленная функция доступна по имени, например: - -```js -//+ run -function showMessage() { - alert( 'Привет всем присутствующим!' ); -} - -*!* -showMessage(); -showMessage(); -*/!* -``` - -Этот код выведет сообщение два раза. Уже здесь видна **главная цель создания функций: избавление от дублирования кода**. - -Если понадобится поменять сообщение или способ его вывода -- достаточно изменить его в одном месте: в функции, которая его выводит. - -## Локальные переменные - -Функция может содержать *локальные* переменные, объявленные через `var`. Такие переменные видны только внутри функции: - -```js -//+ run -function showMessage() { -*!* - var message = 'Привет, я - Вася!'; // локальная переменная -*/!* - - alert( message ); -} - -showMessage(); // 'Привет, я - Вася!' - -alert( message ); // <-- будет ошибка, т.к. переменная видна только внутри -``` - -**Блоки `if/else`, `switch`, `for`, `while`, `do..while` не влияют на область видимости переменных.** - -При объявлении переменной в таких блоках, она всё равно будет видна во всей функции. - -Например: - -```js -//+ no-beautify -function count() { - // переменные i,j не будут уничтожены по окончании цикла - for (*!*var*/!* i = 0; i < 3; i++) { - *!*var*/!* j = i * 2; - } - -*!* - alert( i ); // i=3, последнее значение i, при нём цикл перестал работать - alert( j ); // j=4, последнее значение j, которое вычислил цикл -*/!* -} -``` - -**Неважно, где именно в функции и сколько раз объявляется переменная. Любое объявление срабатывает один раз и распространяется на всю функцию.** - -Объявления переменных в примере выше можно передвинуть вверх, это ни на что не повлияет: - -```js -function count() { -*!* - var i, j; // передвинули объявления var в начало -*/!* - for (i = 0; i < 3; i++) { - j = i * 2; - } - - alert( i ); // i=3 - alert( j ); // j=4 -} -``` - -## Внешние переменные - -Функция может обратиться ко внешней переменной, например: - -```js -//+ run no-beautify -var *!*userName*/!* = 'Вася'; - -function showMessage() { - var message = 'Привет, я ' + *!*userName*/!*; - alert(message); -} - -showMessage(); // Привет, я Вася -``` - -Доступ возможен не только на чтение, но и на запись. При этом, так как переменная внешняя, то изменения будут видны и снаружи функции: - -```js -//+ run -var userName = 'Вася'; - -function showMessage() { - userName = 'Петя'; // (1) присвоение во внешнюю переменную - - var message = 'Привет, я ' + userName; - alert( message ); -} - -showMessage(); - -*!* -alert( userName ); // Петя, значение внешней переменной изменено функцией -*/!* -``` - -Конечно, если бы внутри функции, в строке `(1)`, была бы объявлена своя локальная переменная `var userName`, то все обращения использовали бы её, и внешняя переменная осталась бы неизменной. - -**Переменные, объявленные на уровне всего скрипта, называют *"глобальными переменными"*.** - -В примере выше переменная `userName` -- глобальная. - -Делайте глобальными только те переменные, которые действительно имеют общее значение для вашего проекта, а нужные для решения конкретной задачи -- пусть будут локальными в соответствующей функции. - - -[warn header="Внимание: неявное объявление глобальных переменных!"] - -В старом стандарте JavaScript существовала возможность неявного объявления переменных присвоением значения. - -Например: - -```js -//+ run -function showMessage() { - message = 'Привет'; // без var! -} - -showMessage(); - -alert( message ); // Привет -``` - -В коде выше переменная `message` нигде не объявлена, а сразу присваивается. Скорее всего, программист просто забыл поставить `var`. - -При `use strict` такой код привёл бы к ошибке, но без него переменная будет создана автоматически, причём в примере выше она создаётся не в функции, а на уровне всего скрипта. - -Избегайте этого. - -Здесь опасность даже не в автоматическом создании переменной, а в том, что глобальные переменные должны использоваться тогда, когда действительно нужны "общескриптовые" параметры. - -Забыли `var` в одном месте, потом в другом -- в результате одна функция неожиданно поменяла глобальную переменную, которую использует другая. И поди разберись, кто и когда её поменял, не самая приятная ошибка для отладки. -[/warn] - -В будущем, когда мы лучше познакомимся с основами JavaScript, в главе [](/closures), мы более детально рассмотрим внутренние механизмы работы переменных и функций. - -## Параметры - -При вызове функции ей можно передать данные, которые та использует по своему усмотрению. - -Например, этот код выводит два сообщения: - -```js -//+ run no-beautify -function showMessage(*!*from, text*/!*) { // параметры from, text - - from = "** " + from + " **"; // здесь может быть сложный код оформления - - alert(from + ': ' + text); -} - -*!* -showMessage('Маша', 'Привет!'); -showMessage('Маша', 'Как дела?'); -*/!* -``` - -**Параметры копируются в локальные переменные функции**. - -Например, в коде ниже есть внешняя переменная `from`, значение которой при запуске функции копируется в параметр функции с тем же именем. Далее функция работает уже с параметром: - -```js -//+ run -function showMessage(from, text) { -*!* - from = '**' + from + '**'; // меняем локальную переменную from -*/!* - alert( from + ': ' + text ); -} - -var from = "Маша"; - -showMessage(from, "Привет"); - -alert( from ); // старое значение from без изменений, в функции была изменена копия -``` - -## Аргументы по умолчанию - -Функцию можно вызвать с любым количеством аргументов. - -Если параметр не передан при вызове -- он считается равным `undefined`. - -Например, функцию показа сообщения `showMessage(from, text)` можно вызвать с одним аргументом: - -```js -showMessage("Маша"); -``` - -При этом можно проверить, и если параметр не передан -- присвоить ему значение "по умолчанию": - -```js -//+ run -function showMessage(from, text) { -*!* - if (text === undefined) { - text = 'текст не передан'; - } -*/!* - - alert( from + ": " + text ); -} - -showMessage("Маша", "Привет!"); // Маша: Привет! -*!* -showMessage("Маша"); // Маша: текст не передан -*/!* -``` - -**При объявлении функции необязательные аргументы, как правило, располагают в конце списка.** - -Для указания значения "по умолчанию", то есть, такого, которое используется, если аргумент не указан, используется два способа: - -
    -
  1. Можно проверить, равен ли аргумент `undefined`, и если да -- то записать в него значение по умолчанию. Этот способ продемонстрирован в примере выше.
  2. -
  3. Использовать оператор `||`: - -```js -//+ run -function showMessage(from, text) { - text = text || 'текст не передан'; - - ... -} -``` - -Второй способ считает, что аргумент отсутствует, если передана пустая строка, `0`, или вообще любое значение, которое в логическом контексте является `false`. -
  4. -
- -Если аргументов передано больше, чем надо, например `showMessage("Маша", "привет", 1, 2, 3)`, то ошибки не будет. Но, чтобы получить такие "лишние" аргументы, нужно будет прочитать их из специального объекта `arguments`, который мы рассмотрим в главе [](/arguments-pseudoarray). - -## Возврат значения - -Функция может возвратить результат, который будет передан в вызвавший её код. - -Например, создадим функцию `calcD`, которая будет возвращать дискриминант квадратного уравнения по формуле b2 - 4ac: - -```js -//+ run no-beautify -function calcD(a, b, c) { - *!*return*/!* b*b - 4*a*c; -} - -var test = calcD(-4, 2, 1); -alert(test); // 20 -``` - -**Для возврата значения используется директива `return`.** - -Она может находиться в любом месте функции. Как только до неё доходит управление -- функция завершается и значение передается обратно. - -Вызовов `return` может быть и несколько, например: - -```js -//+ run -function checkAge(age) { - if (age > 18) { - return true; - } else { - return confirm('Родители разрешили?'); - } -} - -var age = prompt('Ваш возраст?'); - -if (checkAge(age)) { - alert( 'Доступ разрешен' ); -} else { - alert( 'В доступе отказано' ); -} -``` - -Директива `return` может также использоваться без значения, чтобы прекратить выполнение и выйти из функции. - -Например: - -```js -function showMovie(age) { - if (!checkAge(age)) { -*!* - return; -*/!* - } - - alert( "Фильм не для всех" ); // (*) - // ... -} -``` - -В коде выше, если сработал `if`, то строка `(*)` и весь код под ней никогда не выполнится, так как `return` завершает выполнение функции. - -[smart header="Значение функции без `return` и с пустым `return`"] -В случае, когда функция не вернула значение или `return` был без аргументов, считается что она вернула `undefined`: - -```js -//+ run -function doNothing() { /* пусто */ } - -alert( doNothing() ); // undefined -``` - -Обратите внимание, никакой ошибки нет. Просто возвращается `undefined`. - -Ещё пример, на этот раз с `return` без аргумента: - -```js -//+ run -function doNothing() { - return; -} - -alert( doNothing() === undefined ); // true -``` - -[/smart] - -## Выбор имени функции [#function-naming] - -Имя функции следует тем же правилам, что и имя переменной. Основное отличие -- оно должно быть глаголом, т.к. функция -- это действие. - -Как правило, используются глагольные префиксы, обозначающие общий характер действия, после которых следует уточнение. - -Функции, которые начинаются с `"show"` -- что-то показывают: - -```js -//+ no-beautify -showMessage(..) // префикс show, "показать" сообщение -``` - -Функции, начинающиеся с `"get"` -- получают, и т.п.: - -```js -//+ no-beautify -getAge(..) // get, "получает" возраст -calcD(..) // calc, "вычисляет" дискриминант -createForm(..) // create, "создает" форму -checkPermission(..) // check, "проверяет" разрешение, возвращает true/false -``` - -Это очень удобно, поскольку взглянув на функцию -- мы уже примерно представляем, что она делает, даже если функцию написал совсем другой человек, а в отдельных случаях -- и какого вида значение она возвращает. - -[smart header="Одна функция -- одно действие"] - -Функция должна делать только то, что явно подразумевается её названием. И это должно быть одно действие. - -Если оно сложное и подразумевает поддействия -- может быть имеет смысл выделить их в отдельные функции? Зачастую это имеет смысл, чтобы лучше структурировать код. - -**...Но самое главное -- в функции не должно быть ничего, кроме самого действия и поддействий, неразрывно связанных с ним.** - -Например, функция проверки данных (скажем, `"validate"`) не должна показывать сообщение об ошибке. Её действие -- проверить. -[/smart] - - -[smart header="Сверхкороткие имена функций"] -Имена функций, которые используются *очень часто*, иногда делают сверхкороткими. - -Например, во фреймворке [jQuery](http://jquery.com) есть функция `$`, во фреймворке [Prototype](http://prototypejs.com) -- функция `$$`, а в библиотеке [LoDash](http://lodash.com/) очень активно используется функция с названием из одного символа подчеркивания `_`. -[/smart] - -## Итого - -Объявление функции имеет вид: - -```js -function имя(параметры, через, запятую) { - код функции -} -``` - -
    -
  • Передаваемые значения копируются в параметры функции и становятся локальными переменными.
  • -
  • Параметры функции копируются в её локальные переменные.
  • -
  • Можно объявить новые локальные переменые при помощи `var`.
  • -
  • Значение возвращается оператором `return ...`.
  • -
  • Вызов `return` тут же прекращает функцию.
  • -
  • Если `return;` вызван без значения, или функция завершилась без `return`, то её результат равен `undefined`.
  • -
- -При обращении к необъявленной переменной функция будет искать внешнюю переменную с таким именем, но лучше, если функция использует только локальные переменные: - -
    -
  • Это делает очевидным общий поток выполнения -- что передаётся в функцию и какой получаем результат.
  • -
  • Это предотвращает возможные конфликты доступа, когда две функции, возможно написанные в разное время или разными людьми, неожиданно друг для друга меняют одну и ту же внешнюю переменную.
  • -
- - - -Именование функций: - -
    -
  • Имя функции должно понятно и чётко отражать, что она делает. Увидев её вызов в коде, вы должны тут же понимать, что она делает.
  • -
  • Функция -- это действие, поэтому для имён функций, как правило, используются глаголы.
  • -
- -Функции являются основными строительными блоками скриптов. Мы будем неоднократно возвращаться к ним и изучать все более и более глубоко. - - diff --git a/1-js/2-first-steps/18-function-declaration-expression/article.md b/1-js/2-first-steps/18-function-declaration-expression/article.md deleted file mode 100644 index 6449b93a..00000000 --- a/1-js/2-first-steps/18-function-declaration-expression/article.md +++ /dev/null @@ -1,366 +0,0 @@ -# Функциональные выражения - -В JavaScript функция является значением, таким же как строка или число. - -Как и любое значение, объявленную функцию можно вывести, вот так: - -```js -//+ run -function sayHi() { - alert( "Привет" ); -} - -*!* -alert( sayHi ); // выведет код функции -*/!* -``` - -Обратим внимание на то, что в последней строке после `sayHi` нет скобок. То есть, функция не вызывается, а просто выводится на экран. - -**Функцию можно скопировать в другую переменную:** - -```js -//+ run no-beautify -function sayHi() { // (1) - alert( "Привет" ); -} - -var func = sayHi; // (2) -func(); // Привет // (3) - -sayHi = null; -sayHi(); // ошибка (4) -``` - -
    -
  1. Объявление `(1)` как бы говорит интерпретатору "создай функцию и помести её в переменную `sayHi`
  2. -
  3. В строке `(2)` мы копируем функцию в новую переменную `func`. Ещё раз обратите внимание: после `sayHi` нет скобок. Если бы они были, то вызов `var func = sayHi()` записал бы в `func` *результат* работы `sayHi()` (кстати, чему он равен? правильно, `undefined`, ведь внутри `sayHi` нет `return`).
  4. -
  5. На момент `(3)` функцию можно вызывать и как `sayHi()` и как `func()`
  6. -
  7. ...Однако, в любой момент значение переменной можно поменять. При этом, если оно не функция, то вызов `(4)` выдаст ошибку.
  8. -
- -Обычные значения, такие как числа или строки, представляют собой *данные*. А функцию можно воспринимать как *действие*. - -Это действие можно запустить через скобки `()`, а можно и скопировать в другую переменную, как было продемонстрировано выше. - - -## Объявление Function Expression [#function-expression] - -Существует альтернативный синтаксис для объявления функции, который ещё более наглядно показывает, что функция -- это всего лишь разновидность значения переменной. - -Он называется "Function Expression" (функциональное выражение) и выглядит так: - -```js -//+ run -var f = function(параметры) { - // тело функции -}; -``` - -Например: - -```js -//+ run -var sayHi = function(person) { - alert( "Привет, " + person ); -}; - -sayHi('Вася'); -``` - -## Сравнение с Function Declaration - -"Классическое" объявление функции, о котором мы говорили до этого, вида `function имя(параметры) {...}`, называется в спецификации языка "Function Declaration". - -
    -
  • *Function Declaration* -- функция, объявленная в основном потоке кода.
  • -
  • *Function Expression* -- объявление функции в контексте какого-либо выражения, например присваивания.
  • -
- -Несмотря на немного разный вид, по сути две эти записи делают одно и то же: - -```js -// Function Declaration -function sum(a, b) { - return a + b; -} - -// Function Expression -var sum = function(a, b) { - return a + b; -} -``` - -Оба этих объявления говорят интерпретатору: "объяви переменную `sum`, создай функцию с указанными параметрами и кодом и сохрани её в `sum`". - -**Основное отличие между ними: функции, объявленные как Function Declaration, создаются интерпретатором до выполнения кода.** - -Поэтому их можно вызвать *до* объявления, например: - -```js -//+ run refresh untrusted -*!* -sayHi("Вася"); // Привет, Вася -*/!* - -function sayHi(name) { - alert( "Привет, " + name ); -} -``` - -А если бы это было объявление Function Expression, то такой вызов бы не сработал: - -```js -//+ run refresh untrusted -*!* -sayHi("Вася"); // ошибка! -*/!* - -var sayHi = function(name) { - alert( "Привет, " + name ); -} -``` - -Это из-за того, что JavaScript перед запуском кода ищет в нём Function Declaration (их легко найти: они не являются частью выражений и начинаются со слова `function`) и обрабатывает их. - -А Function Expression создаются в процессе выполнении выражения, в котором созданы, в данном случае -- функция будет создана при операции присваивания `sayHi = function...` - -Как правило, возможность Function Declaration вызвать функцию до объявления -- это удобно, так как даёт больше свободы в том, как организовать свой код. - -Можно расположить функции внизу, а их вызов -- сверху или наоборот. - -### Условное объявление функции [#bad-conditional-declaration] - -В некоторых случаях "дополнительное удобство" Function Declaration может сослужить плохую службу. - -Например, попробуем, в зависимости от условия, объявить функцию `sayHi` по-разному: - -```js -//+ run -var age = +prompt("Сколько вам лет?", 20); - -if (age >= 18) { - function sayHi() { - alert( 'Прошу вас!' ); - } -} else { - function sayHi() { - alert( 'До 18 нельзя' ); - } -} - -sayHi(); -``` - -При вводе `20` в примере выше в любом браузере, кроме Firefox, мы увидим, что условное объявление не работает. Срабатывает `"До 18 нельзя"`, несмотря на то, что `age = 20`. - -В чём дело? Чтобы ответить на этот вопрос -- вспомним, как работают функции. - -
    -
  1. Function Declaration обрабатываются перед запуском кода. Интерпретатор сканирует код и создает из таких объявлений функции. При этом второе объявление перезапишет первое. -
  2. -
  3. Дальше, во время выполнения, объявления Function Declaration игнорируются (они уже были обработаны). Это как если бы код был таким: - -```js -function sayHi() { - alert( 'Прошу вас!' ); -} - -function sayHi() { - alert( 'До 18 нельзя' ); -} - -var age = 20; - -if (age >= 18) { - /* объявление было обработано ранее */ -} else { - /* объявление было обработано ранее */ -} - -*!* -sayHi(); // "До 18 нельзя", сработает всегда вторая функция -*/!* -``` - -...То есть, от `if` здесь уже ничего не зависит. По-разному объявить функцию, в зависимости от условия, не получилось. -
  4. -
- -Такое поведение соответствует современному стандарту. На момент написания этого раздела ему следуют все браузеры, кроме, как ни странно, Firefox. - -**Вывод: для условного объявления функций Function Declaration не годится.** - -А что, если использовать Function Expression? - -```js -//+ run -var age = prompt('Сколько вам лет?'); - -var sayHi; - -if (age >= 18) { - sayHi = function() { - alert( 'Прошу Вас!' ); - } -} else { - sayHi = function() { - alert( 'До 18 нельзя' ); - } -} - -sayHi(); -``` - -Или даже так: - -```js -//+ run no-beautify -var age = prompt('Сколько вам лет?'); - -var sayHi = (age >= 18) ? - function() { alert('Прошу Вас!'); } : - function() { alert('До 18 нельзя'); }; - -sayHi(); -``` - -Оба этих варианта работают правильно, поскольку, в зависимости от условия, создаётся именно та функция, которая нужна. - -### Анонимные функции - -Взглянем ещё на один пример. - -Функция `ask(question, yes, no)` предназначена для выбора действия в зависимости от результата `f`. - -Она выводит вопрос на подтверждение `question` и, в зависимости от согласия пользователя, вызывает `yes` или `no`: - -```js -//+ run -*!* -function ask(question, yes, no) { - if (confirm(question)) yes() - else no(); - } -*/!* - -function showOk() { - alert( "Вы согласились." ); -} - -function showCancel() { - alert( "Вы отменили выполнение." ); -} - -// использование -ask("Вы согласны?", showOk, showCancel); -``` - -Какой-то очень простой код, не правда ли? Зачем, вообще, может понадобиться такая `ask`? - -...Но при работе со страницей такие функции как раз очень востребованы, только вот спрашивают они не простым `confirm`, а выводят более красивое окно с вопросом и могут интеллектуально обработать ввод посетителя. Но это всё в своё время. - -Здесь обратим внимание на то, что то же самое можно написать более коротко: - -```js -//+ run no-beautify -function ask(question, yes, no) { - if (confirm(question)) yes() - else no(); -} - -*!* -ask( - "Вы согласны?", - function() { alert("Вы согласились."); }, - function() { alert("Вы отменили выполнение."); } -); -*/!* -``` - -Здесь функции объявлены прямо внутри вызова `ask(...)`, даже без присвоения им имени. - -**Функциональное выражение, которое не записывается в переменную, называют [анонимной функцией](http://ru.wikipedia.org/wiki/%D0%90%D0%BD%D0%BE%D0%BD%D0%B8%D0%BC%D0%BD%D0%B0%D1%8F_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F).** - -Действительно, зачем нам записывать функцию в переменную, если мы не собираемся вызывать её ещё раз? Можно просто объявить непосредственно там, где функция нужна. - -Такого рода код возникает естественно, он соответствует "духу" JavaScript. - -## new Function - -Существует ещё один способ создания функции, который используется очень редко, но упомянем и его для полноты картины. - -Он позволяет создавать функцию полностью "на лету" из строки, вот так: - -```js -//+ run -var sum = new Function('a,b', ' return a+b; '); - -var result = sum(1, 2); -alert( result ); // 3 -``` - -То есть, функция создаётся вызовом `new Function(params, code)`: -
-
`params`
-
Параметры функции через запятую в виде строки.
-
`code`
-
Код функции в виде строки.
-
- -Таким образом можно конструировать функцию, код которой неизвестен на момент написания программы, но строка с ним генерируется или подгружается динамически во время её выполнения. - -Пример использования -- динамическая компиляция шаблонов на JavaScript, мы встретимся с ней позже, при работе с интерфейсами. - -## Итого - -Функции в JavaScript являются значениями. Их можно присваивать, передавать, создавать в любом месте кода. - -
    -
  • Если функция объявлена в *основном потоке кода*, то это Function Declaration.
  • -
  • Если функция создана как *часть выражения*, то это Function Expression.
  • -
- -Между этими двумя основными способами создания функций есть следующие различия: - - - - - - - - - - - - - - - - - - - - - - -
Function DeclarationFunction Expression
Время созданияДо выполнения первой строчки кода.Когда управление достигает строки с функцией.
Можно вызвать до объявления `Да` (т.к. создаётся заранее)`Нет`
Условное объявление в `if``Не работает``Работает`
- -Иногда в коде начинающих разработчиков можно увидеть много Function Expression. Почему-то, видимо, не очень понимая происходящее, функции решают создавать как `var func = function()`, но в большинстве случаев обычное объявление функции -- лучше. - -**Если нет явной причины использовать Function Expression -- предпочитайте Function Declaration.** - -Сравните по читаемости: - -```js -//+ no-beautify -// Function Expression -var f = function() { ... } - -// Function Declaration -function f() { ... } -``` - -Function Declaration короче и лучше читается. Дополнительный бонус -- такие функции можно вызывать до того, как они объявлены. - -Используйте Function Expression только там, где это действительно нужно и удобно. diff --git a/1-js/2-first-steps/19-recursion/1-sum-to/solution.md b/1-js/2-first-steps/19-recursion/1-sum-to/solution.md deleted file mode 100644 index 791c4b3d..00000000 --- a/1-js/2-first-steps/19-recursion/1-sum-to/solution.md +++ /dev/null @@ -1,45 +0,0 @@ -Решение **с использованием цикла**: - -```js -//+ run -function sumTo(n) { - var sum = 0; - for (var i = 1; i <= n; i++) { - sum += i; - } - return sum; -} - -alert( sumTo(100) ); -``` - -Решение через **рекурсию**: - -```js -//+ run -function sumTo(n) { - if (n == 1) return 1; - return n + sumTo(n - 1); -} - -alert( sumTo(100) ); -``` - -Решение **по формуле**: `sumTo(n) = n*(n+1)/2`: - -```js -//+ run -function sumTo(n) { - return n * (n + 1) / 2; -} - -alert( sumTo(100) ); -``` - -P.S. Надо ли говорить, что решение по формуле работает быстрее всех? Это очевидно. Оно использует всего три операции для любого `n`, а цикл и рекурсия требуют как минимум `n` операций сложения. - -Вариант с циклом -- второй по скорости. Он быстрее рекурсии, так как операций сложения столько же, но нет дополнительных вычислительных затрат на организацию вложенных вызовов. - -Рекурсия в данном случае работает медленнее всех. - -P.P.S. Существует ограничение глубины вложенных вызовов, поэтому рекурсивный вызов `sumTo(100000)` выдаст ошибку. \ No newline at end of file diff --git a/1-js/2-first-steps/19-recursion/1-sum-to/task.md b/1-js/2-first-steps/19-recursion/1-sum-to/task.md deleted file mode 100644 index 9e7e8e8f..00000000 --- a/1-js/2-first-steps/19-recursion/1-sum-to/task.md +++ /dev/null @@ -1,34 +0,0 @@ -# Вычислить сумму чисел до данного - -[importance 5] - -Напишите функцию `sumTo(n)`, которая для данного `n` вычисляет сумму чисел от 1 до `n`, например: - -```js -//+ no-beautify -sumTo(1) = 1 -sumTo(2) = 2 + 1 = 3 -sumTo(3) = 3 + 2 + 1 = 6 -sumTo(4) = 4 + 3 + 2 + 1 = 10 -... -sumTo(100) = 100 + 99 + ... + 2 + 1 = 5050 -``` - -Сделайте три варианта решения: -
    -
  1. С использованием цикла.
  2. -
  3. Через рекурсию, т.к. `sumTo(n) = n + sumTo(n-1)` для `n > 1`.
  4. -
  5. С использованием формулы для суммы [арифметической прогрессии](http://ru.wikipedia.org/wiki/%D0%90%D1%80%D0%B8%D1%84%D0%BC%D0%B5%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B5%D1%81%D1%81%D0%B8%D1%8F).
  6. -
- -Пример работы вашей функции: - -```js -function sumTo(n) { /*... ваш код ... */ } - -alert( sumTo(100) ); // 5050 -``` - -**Какой вариант решения самый быстрый? Самый медленный? Почему?** - -**Можно ли при помощи рекурсии посчитать `sumTo(100000)`? Если нет, то почему?** diff --git a/1-js/2-first-steps/19-recursion/2-factorial/solution.md b/1-js/2-first-steps/19-recursion/2-factorial/solution.md deleted file mode 100644 index f3811e2c..00000000 --- a/1-js/2-first-steps/19-recursion/2-factorial/solution.md +++ /dev/null @@ -1,25 +0,0 @@ -По свойствам факториала, как описано в условии, `n!` можно записать как `n * (n-1)!`. - -То есть, результат функции для `n` можно получить как `n`, умноженное на результат функции для `n-1`, и так далее до `1!`: - -```js -//+ run -function factorial(n) { - return (n != 1) ? n * factorial(n - 1) : 1; -} - -alert( factorial(5) ); // 120 -``` - -Базисом рекурсии является значение `1`. А можно было бы сделать базисом и `0`. Тогда код станет чуть короче: - -```js -//+ run -function factorial(n) { - return n ? n * factorial(n - 1) : 1; -} - -alert( factorial(5) ); // 120 -``` - -В этом случае вызов `factorial(1)` сведётся к `1*factorial(0)`, будет дополнительный шаг рекурсии. diff --git a/1-js/2-first-steps/19-recursion/2-factorial/task.md b/1-js/2-first-steps/19-recursion/2-factorial/task.md deleted file mode 100644 index 59579043..00000000 --- a/1-js/2-first-steps/19-recursion/2-factorial/task.md +++ /dev/null @@ -1,29 +0,0 @@ -# Вычислить факториал - -[importance 4] - -*Факториа́л числа* -- это число, умноженное на "себя минус один", затем на "себя минус два" и так далее, до единицы. Обозначается `n!` - -Определение факториала можно записать как: - -```js -n! = n * (n - 1) * (n - 2) * ...*1 -``` - -Примеры значений для разных `n`: - -```js -1! = 1 -2! = 2 * 1 = 2 -3! = 3 * 2 * 1 = 6 -4! = 4 * 3 * 2 * 1 = 24 -5! = 5 * 4 * 3 * 2 * 1 = 120 -``` - -Задача -- написать функцию `factorial(n)`, которая возвращает факториал числа `n!`, используя рекурсивный вызов. - -```js -alert( factorial(5) ); // 120 -``` - -Подсказка: обратите внимание, что `n!` можно записать как `n * (n-1)!`. Например: `3! = 3*2! = 3*2*1! = 6` diff --git a/1-js/2-first-steps/19-recursion/3-fibonacci-numbers/solution.md b/1-js/2-first-steps/19-recursion/3-fibonacci-numbers/solution.md deleted file mode 100644 index 5c5fbfab..00000000 --- a/1-js/2-first-steps/19-recursion/3-fibonacci-numbers/solution.md +++ /dev/null @@ -1,99 +0,0 @@ -# Вычисление рекурсией (медленное) - -Решение по формуле, используя рекурсию: - -```js -//+ run -function fib(n) { - return n <= 1 ? n : fib(n - 1) + fib(n - 2); -} - -alert( fib(3) ); // 2 -alert( fib(7) ); // 13 -// fib(77); // не запускаем, подвесит браузер -``` - -При больших значениях `n` оно будет работать очень медленно. Например, `fib(77)` уже будет вычисляться очень долго. - -Это потому, что функция порождает обширное дерево вложенных вызовов. При этом ряд значений вычисляется много раз. Например, посмотрим на отрывок вычислений: - -```js -//+ no-beautify -... -fib(5) = fib(4) + fib(3) -fib(4) = fib(3) + fib(2) -... -``` - -Здесь видно, что значение `fib(3)` нужно одновременно и для `fib(5)` и для `fib(4)`. В коде оно будет вычислено два раза, совершенно независимо. - -Можно это оптимизировать, запоминая уже вычисленные значения, получится гораздо быстрее. Альтернативный вариант -- вообще отказаться от рекурсии, а вместо этого в цикле начать с первых значений `1`, `2`, затем из них получить `fib(3)`, далее `fib(4)`, затем `fib(5)` и так далее, до нужного значения. - -Это решение будет наиболее эффективным. Попробуйте его написать. - -# Алгоритм вычисления в цикле - -Будем идти по формуле слева-направо: - -```js -//+ no-beautify -var a = 1, b = 1; // начальные значения -var c = a + b; // 2 - -/* переменные на начальном шаге: -a b c -1, 1, 2 -*/ -``` - -Теперь следующий шаг, присвоим `a` и `b` текущие 2 числа и получим новое следующее в `c`: - -```js -//+ no-beautify -a = b, b = c; -c = a + b; - -/* стало так (ещё число): - a b c -1, 1, 2, 3 -*/ -``` - -Следующий шаг даст нам ещё одно число последовательности: - -```js -//+ no-beautify -a = b, b = c; -c = a + b; - -/* стало так (ещё число): - a b c -1, 1, 2, 3, 5 -*/ -``` - -Повторять в цикле до тех пор, пока не получим нужное значение. Это гораздо быстрее, чем рекурсия, хотя бы потому что ни одно из чисел не вычисляется дважды. - -P.S. Этот подход к вычислению называется [динамическое программирование снизу-вверх](http://ru.wikipedia.org/wiki/%D0%94%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5). - -# Код для вычисления в цикле - -```js -//+ run -function fib(n) { - var a = 1, - b = 1; - for (var i = 3; i <= n; i++) { - var c = a + b; - a = b; - b = c; - } - return b; -} - -alert( fib(3) ); // 2 -alert( fib(7) ); // 13 -alert( fib(77) ); // 5527939700884757 -``` - -Цикл здесь начинается с `i=3`, так как первое и второе числа Фибоначчи заранее записаны в переменные `a=1`, `b=1`. diff --git a/1-js/2-first-steps/19-recursion/3-fibonacci-numbers/task.md b/1-js/2-first-steps/19-recursion/3-fibonacci-numbers/task.md deleted file mode 100644 index b05734a0..00000000 --- a/1-js/2-first-steps/19-recursion/3-fibonacci-numbers/task.md +++ /dev/null @@ -1,24 +0,0 @@ -# Числа Фибоначчи - -[importance 5] - -Последовательность [чисел Фибоначчи](http://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D0%BB%D0%B0_%D0%A4%D0%B8%D0%B1%D0%BE%D0%BD%D0%B0%D1%87%D1%87%D0%B8) имеет формулу Fn = Fn-1 + Fn-2. То есть, следующее число получается как сумма двух предыдущих. - -Первые два числа равны `1`, затем `2(1+1)`, затем `3(1+2)`, `5(2+3)` и так далее: `1, 1, 2, 3, 5, 8, 13, 21...`. - -Числа Фибоначчи тесно связаны с [золотым сечением](http://ru.wikipedia.org/wiki/%D0%97%D0%BE%D0%BB%D0%BE%D1%82%D0%BE%D0%B5_%D1%81%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D0%B5) и множеством природных явлений вокруг нас. - -Напишите функцию `fib(n)`, которая возвращает `n-е` число Фибоначчи. Пример работы: - -```js -//+ no-beautify -function fib(n) { /* ваш код */ } - -alert( fib(3) ); // 2 -alert( fib(7) ); // 13 -alert( fib(77)); // 5527939700884757 -``` - -**Все запуски функций из примера выше должны срабатывать быстро.** - - \ No newline at end of file diff --git a/1-js/2-first-steps/19-recursion/article.md b/1-js/2-first-steps/19-recursion/article.md deleted file mode 100644 index 144e1cad..00000000 --- a/1-js/2-first-steps/19-recursion/article.md +++ /dev/null @@ -1,243 +0,0 @@ -# Рекурсия, стек - -В коде функции могут вызывать другие функции для выполнения подзадач. - -Частный случай подвызова -- когда функция вызывает сама себя. Это называется *рекурсией*. - -Рекурсия используется для ситуаций, когда выполнение одной сложной задачи можно представить как некое действие в совокупности с решением той же задачи в более простом варианте. - -Сейчас мы посмотрим примеры. - -Рекурсия -- общая тема программирования, не относящаяся напрямую к JavaScript. Если вы разрабатывали на других языках или изучали программирование раньше в ВУЗе, то наверняка уже знаете, что это такое. - -Эта глава предназначена для читателей, которые пока с этой темой незнакомы и хотят лучше разобраться в том, как работают функции. - -[cut] - - -## Степень pow(x, n) через рекурсию - -В качестве первого примера использования рекурсивных вызовов -- рассмотрим задачу возведения числа `x` в натуральную степень `n`. - -Её можно представить как совокупность более простого действия и более простой задачи того же типа вот так: - -```js -pow(x, n) = x * pow(x, n - 1) -``` - -То есть, xn = x * xn-1. - -Например, вычислим `pow(2, 4)`, последовательно переходя к более простой задаче: - -
    -
  1. `pow(2, 4) = 2 * pow(2, 3)`
  2. -
  3. `pow(2, 3) = 2 * pow(2, 2)`
  4. -
  5. `pow(2, 2) = 2 * pow(2, 1)`
  6. -
  7. `pow(2, 1) = 2`
  8. -
- -На шаге 1 нам нужно вычислить `pow(2,3)`, поэтому мы делаем шаг 2, дальше нам нужно `pow(2,2)`, мы делаем шаг 3, затем шаг 4, и на нём уже можно остановиться, ведь очевидно, что результат возведения числа в степень 1 -- равен самому числу. - -Далее, имея результат на шаге 4, он подставляется обратно в шаг 3, затем имеем `pow(2,2)` -- подставляем в шаг 2 и на шаге 1 уже получаем результат. - -Этот алгоритм на JavaScript: - -```js -//+ run -function pow(x, n) { - if (n != 1) { // пока n != 1, сводить вычисление pow(x,n) к pow(x,n-1) - return x * pow(x, n - 1); - } else { - return x; - } -} - -alert( pow(2, 3) ); // 8 -``` - -Говорят, что "функция `pow` *рекурсивно вызывает сама себя*" до `n == 1`. - -Значение, на котором рекурсия заканчивается называют *базисом рекурсии*. В примере выше базисом является `1`. - -Общее количество вложенных вызовов называют *глубиной рекурсии*. В случае со степенью, всего будет `n` вызовов. - -Максимальная глубина рекурсии в браузерах ограничена, точно можно рассчитывать на `10000` вложенных вызовов, но некоторые интерпретаторы допускают и больше. - -Итак, рекурсию используют, когда вычисление функции можно свести к её более простому вызову, а его -- ещё к более простому, и так далее, пока значение не станет очевидно. - -## Контекст выполнения, стек - -Теперь мы посмотрим, как работают рекурсивные вызовы. Для этого мы рассмотрим, как вообще работают функции, что происходит при вызове. - -**У каждого вызова функции есть свой "контекст выполнения" (execution context).** - -Контекст выполнения -- это служебная информация, которая соответствует текущему запуску функции. Она включает в себя локальные переменные функции и конкретное место в коде, на котором находится интерпретатор. - -Например, для вызова `pow(2, 3)` из примера выше будет создан контекст выполнения, который будет хранить переменные `x = 2, n = 3`. Мы схематично обозначим его так: - -
    -
  • Контекст: { x: 2, n: 3, строка 1 }
  • -
- -Далее функция `pow` начинает выполняться. Вычисляется выражение `n != 1` -- оно равно `true`, ведь в текущем контексте `n=3`. Поэтому задействуется первая ветвь `if` : - -```js -function pow(x, n) { - if (n != 1) { // пока n != 1 сводить вычисление pow(x,n) к pow(x,n-1) -*!* - return x * pow(x, n - 1); -*/!* - } else { - return x; - } -} -``` - -Чтобы вычислить выражение `x * pow(x, n-1)`, требуется произвести запуск `pow` с новыми аргументами. - -**При любом вложенном вызове JavaScript запоминает текущий контекст выполнения в специальной внутренней структуре данных -- "стеке контекстов".** - -Затем интерпретатор приступает к выполнению вложенного вызова. - -В данном случае вызывается та же `pow`, однако это абсолютно неважно. Для любых функций процесс одинаков. - -Для нового вызова создаётся свой контекст выполнения, и управление переходит в него, а когда он завершён -- старый контекст достаётся из стека и выполнение внешней функции возобновляется. - -Разберём происходящее с контекстами более подробно, начиная с вызова `(*)`: - -```js -//+ run -function pow(x, n) { - if (n != 1) { // пока n!=1 сводить вычисление pow(..n) к pow(..n-1) - return x * pow(x, n - 1); - } else { - return x; - } -} - -*!* -alert( pow(2, 3) ); // (*) -*/!* -``` - -
-
`pow(2, 3)`
-
Запускается функция `pow`, с аргументами `x=2`, `n=3`. Эти переменные хранятся в контексте выполнения, схематично изображённом ниже: - -
    -
  • Контекст: { x: 2, n: 3, строка 1 }
  • -
-Выполнение в этом контексте продолжается, пока не встретит вложенный вызов в строке 3. -
-
`pow(2, 2)`
-
В строке `3` происходит вложенный вызов `pow` с аргументами `x=2`, `n=2`. Текущий контекст сохраняется в стеке, а для вложеннного вызова создаётся новый контекст (выделен жирным ниже): - -
    -
  • Контекст: { x: 2, n: 3, строка 3 }
  • -
  • Контекст: { x: 2, n: 2, строка 1 }
  • -
-Обратим внимание, что контекст включает в себя не только переменные, но и место в коде, так что когда вложенный вызов завершится -- можно будет легко вернуться назад. - -Слово "строка" здесь условно, на самом деле, конечно, запомнено более точное место в цепочке команд. -
-
`pow(2, 1)`
-
Опять вложенный вызов в строке `3`, на этот раз -- с аргументами `x=2`, `n=1`. Создаётся новый текущий контекст, предыдущий добавляется в стек: -
    -
  • Контекст: { x: 2, n: 3, строка 3 }
  • -
  • Контекст: { x: 2, n: 2, строка 3 }
  • -
  • Контекст: { x: 2, n: 1, строка 1 }
  • -
-На текущий момент в стеке уже два старых контекста. -
-
Выход из `pow(2, 1)`.
-
При выполнении `pow(2, 1)`, в отличие от предыдущих запусков, выражение `n != 1` будет равно `false`, поэтому сработает вторая ветка `if..else`: - -```js -function pow(x, n) { - if (n != 1) { - return x * pow(x, n - 1); - } else { -*!* - return x; // первая степень числа равна самому числу -*/!* - } -} -``` - -Здесь вложенных вызовов нет, так что функция заканчивает свою работу, возвращая `2`. Текущий контекст больше не нужен и удаляется из памяти, из стека восстанавливается предыдущий: - -
    -
  • Контекст: { x: 2, n: 3, строка 3 }
  • -
  • Контекст: { x: 2, n: 2, строка 3 }
  • -
-Возобновляется обработка внешнего вызова `pow(2, 2)`. -
-
Выход из `pow(2, 2)`.
-
...И теперь уже `pow(2, 2)` может закончить свою работу, вернув `4`. Восстанавливается контекст предыдущего вызова: -
    -
  • Контекст: { x: 2, n: 3, строка 3 }
  • -
-Возобновляется обработка внешнего вызова `pow(2, 3)`. -
-
Выход из `pow(2, 3)`.
-
Самый внешний вызов заканчивает свою работу, его результат: `pow(2, 3) = 8`.
-
- -Глубина рекурсии в данном случае составила: **3**. - -Как видно из иллюстраций выше, глубина рекурсии равна максимальному числу контекстов, одновременно хранимых в стеке. - -Обратим внимание на требования к памяти. Рекурсия приводит к хранению всех данных для неоконченных внешних вызовов в стеке, в данном случае это приводит к тому, что возведение в степень `n` хранит в памяти `n` различных контекстов. - -Реализация возведения в степень через цикл гораздо более экономна: - -```js -function pow(x, n) { - var result = x; - for (var i = 1; i < n; i++) { - result *= x; - } - return result; -} -``` - -У такой функции `pow` будет один контекст, в котором будут последовательно меняться значения `i` и `result`. - -**Любая рекурсия может быть переделана в цикл. Как правило, вариант с циклом будет эффективнее.** - -Но переделка рекурсии в цикл может быть нетривиальной, особенно когда в функции, в зависимости от условий, используются различные рекурсивные подвызовы, когда ветвление более сложное. - -## Итого - -Рекурсия -- это когда функция вызывает сама себя, как правило, с другими аргументами. - -Существуют много областей применения рекурсивных вызовов. Здесь мы посмотрели на один из них -- решение задачи путём сведения её к более простой (с меньшими аргументами), но также рекурсия используется для работы с "естественно рекурсивными" структурами данных, такими как HTML-документы, для "глубокого" копирования сложных объектов. - -Есть и другие применения, с которыми мы встретимся по мере изучения JavaScript. - -Здесь мы постарались рассмотреть происходящее достаточно подробно, однако, если пожелаете, допустимо временно забежать вперёд и открыть главу [](/debugging-chrome), с тем чтобы при помощи отладчика построчно пробежаться по коду и посмотреть стек на каждом шаге. Отладчик даёт к нему доступ. - - - -[head] - -[/head] diff --git a/1-js/2-first-steps/2-external-script/1-hello-alert-ext/alert.js b/1-js/2-first-steps/2-external-script/1-hello-alert-ext/alert.js deleted file mode 100644 index 2c4f96f5..00000000 --- a/1-js/2-first-steps/2-external-script/1-hello-alert-ext/alert.js +++ /dev/null @@ -1 +0,0 @@ -alert('Я - JavaScript!'); \ No newline at end of file diff --git a/1-js/2-first-steps/2-external-script/1-hello-alert-ext/index.html b/1-js/2-first-steps/2-external-script/1-hello-alert-ext/index.html deleted file mode 100644 index b00b9f8a..00000000 --- a/1-js/2-first-steps/2-external-script/1-hello-alert-ext/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/2-first-steps/2-external-script/1-hello-alert-ext/solution.md b/1-js/2-first-steps/2-external-script/1-hello-alert-ext/solution.md deleted file mode 100644 index bef2fc5b..00000000 --- a/1-js/2-first-steps/2-external-script/1-hello-alert-ext/solution.md +++ /dev/null @@ -1,12 +0,0 @@ -Код для HTML-файла: - -```html - -``` - -Для файла `alert.js` из той же директории: - -```js -//+ src="alert.js" -``` - diff --git a/1-js/2-first-steps/2-external-script/1-hello-alert-ext/task.md b/1-js/2-first-steps/2-external-script/1-hello-alert-ext/task.md deleted file mode 100644 index 93cf1b4e..00000000 --- a/1-js/2-first-steps/2-external-script/1-hello-alert-ext/task.md +++ /dev/null @@ -1,7 +0,0 @@ -# Вывести alert внешним скриптом - -[importance 5] - -Возьмите решение предыдущей задачи [](/task/hello-alert) и вынесите скрипт во внешний файл `alert.js`, который расположите в той же директории. - -Откройте страницу и проверьте, что вывод сообщения всё ещё работает. \ No newline at end of file diff --git a/1-js/2-first-steps/2-external-script/2-async-defer-first/solution.md b/1-js/2-first-steps/2-external-script/2-async-defer-first/solution.md deleted file mode 100644 index 089a87e0..00000000 --- a/1-js/2-first-steps/2-external-script/2-async-defer-first/solution.md +++ /dev/null @@ -1,6 +0,0 @@ -Ответы: -
    -
  1. Первым выполнится `big.js`, это нормальная последовательность выполнения подряд идущих скриптов.
  2. -
  3. Первым выполнится `small.js`, так как скрипты из-за `async` ведут себя совершенно независимо друг от друга, страница тоже от них не зависит.
  4. -
  5. Первым выполнится `big.js`, так как скрипты, подключённые через `defer`, сохраняют порядок выполнения относительно друг друга.
  6. -
\ No newline at end of file diff --git a/1-js/2-first-steps/2-external-script/2-async-defer-first/task.md b/1-js/2-first-steps/2-external-script/2-async-defer-first/task.md deleted file mode 100644 index 72405f9a..00000000 --- a/1-js/2-first-steps/2-external-script/2-async-defer-first/task.md +++ /dev/null @@ -1,27 +0,0 @@ -# Какой скрипт выполнится первым? - -[importance 4] - -В примере ниже подключены два скрипта `small.js` и `big.js`. - -Если предположить, что `small.js` загружается гораздо быстрее, чем `big.js` -- какой выполнится первым? - -```html - - -``` - -А вот так? - -```html - - -``` - -А так? - -```html - - -``` - diff --git a/1-js/2-first-steps/2-external-script/article.md b/1-js/2-first-steps/2-external-script/article.md deleted file mode 100644 index 41a9a3b7..00000000 --- a/1-js/2-first-steps/2-external-script/article.md +++ /dev/null @@ -1,251 +0,0 @@ -# Внешние скрипты, порядок исполнения - -Если JavaScript-кода много -- его выносят в отдельный файл, который подключается в HTML: - -```html - -``` - -Здесь `/path/to/script.js` -- это абсолютный путь к файлу, содержащему скрипт (из корня сайта). - -Браузер сам скачает скрипт и выполнит. - -Можно указать и полный URL, например: - -```html - -``` - -Вы также можете использовать путь относительно текущей страницы. Например, `src="lodash.js"` обозначает файл из текущей директории. - -Чтобы подключить несколько скриптов, используйте несколько тегов: - -```html - - -... -``` - -[smart] -Как правило, в HTML пишут только самые простые скрипты, а сложные выносят в отдельный файл. - -Браузер скачает его только первый раз и в дальнейшем, при правильной настройке сервера, будет брать из своего [кеша](http://ru.wikipedia.org/wiki/%D0%9A%D1%8D%D1%88). - -Благодаря этому один и тот же большой скрипт, содержащий, к примеру, библиотеку функций, может использоваться на разных страницах без полной перезагрузки с сервера. -[/smart] - - -[warn header="Если указан атрибут `src`, то содержимое тега игнорируется."] - -В одном теге `SCRIPT` нельзя одновременно подключить внешний скрипт и указать код. - -Вот так не cработает: - -```html - -``` - -Нужно выбрать: либо `SCRIPT` идёт с `src`, либо содержит код. Тег выше следует разбить на два: один -- с `src`, другой -- с кодом, вот так: - -```html - - -``` - -[/warn] - -## Асинхронные скрипты: defer/async - -Браузер загружает и отображает HTML постепенно. Особенно это заметно при медленном интернет-соединении: браузер не ждёт, пока страница загрузится целиком, а показывает ту часть, которую успел загрузить. - -Если браузер видит тег ` -*/!* - -

Кролики посчитаны!

- - - - -``` - -Такое поведение называют "синхронным". Как правило, оно вполне нормально, но есть важное следствие. - -**Если скрипт -- внешний, то пока браузер не выполнит его, он не покажет часть страницы под ним.** - -То есть, в таком документе, пока не загрузится и не выполнится `big.js`, содержимое `` будет скрыто: - -```html - - -*!* - -*/!* - - - Этот текст не будет показан, пока браузер не выполнит big.js. - - -``` - -И здесь вопрос -- действительно ли мы этого хотим? То есть, действительно ли оставшуюся часть страницы нельзя показывать до загрузки скрипта? - -Есть ситуации, когда мы не только НЕ хотим такой задержки, но она даже опасна. - -Например, если мы подключаем внешний скрипт, который показывает рекламу или вставляет счётчик посещений, а затем идёт наша страница. Конечно, неправильно, что пока счётчик или реклама не подгрузятся -- оставшаяся часть страницы не показывается. Счётчик посещений не должен никак задерживать отображение страницы сайта. Реклама тоже не должна тормозить сайт и нарушать его функционал. - -А что, если сервер, с которого загружается внешний скрипт, перегружен? Посетитель в этом случае может ждать очень долго! - -Вот пример, с подобным скриптом (стоит искусственная задержка загрузки): - -```html - -

Важная информация не покажется, пока не загрузится скрипт.

- - - -

...Важная информация!

-``` - -Что делать? - -Можно поставить все подобные скрипты в конец страницы -- это уменьшит проблему, но не избавит от неё полностью, если скриптов несколько. Допустим, в конце страницы 3 скрипта, и первый из них тормозит -- получается, другие два его будут ждать -- тоже нехорошо. - -Кроме того, браузер дойдёт до скриптов, расположенных в конце страницы, они начнут грузиться только тогда, когда вся страница загрузится. А это не всегда правильно. Например, счётчик посещений наиболее точно сработает, если загрузить его пораньше. - -Поэтому "расположить скрипты внизу" -- не лучший выход. - -Кардинально решить эту проблему помогут атрибуты `async` или `defer`: -
-
Атрибут `async`
-
Поддерживается всеми браузерами, кроме IE9-. Скрипт выполняется полностью асинхронно. То есть, при обнаружении ` - -``` - -А в таком коде (с `defer`) первым сработает всегда `1.js`, а скрипт `2.js`, даже если загрузился раньше, будет его ждать. - -```html - - -``` - -Поэтому атрибут `defer` используют в тех случаях, когда второй скрипт `2.js` зависит от первого `1.js`, к примеру -- использует что-то, описанное первым скриптом. - -Второе отличие -- скрипт с `defer` сработает, когда весь HTML-документ будет обработан браузером. - -Например, если документ достаточно большой... -```html - - - -Много много много букв -``` - -...То скрипт `async.js` выполнится, как только загрузится -- возможно, до того, как весь документ готов. А `defer.js` подождёт готовности всего документа. - -Это бывает удобно, когда мы в скрипте хотим работать с документом, и должны быть уверены, что он полностью получен. -
-
- -[smart header="`async` вместе с `defer`"] -При одновременном указании `async` и `defer` в современных браузерах будет использован только `async`, в IE9- -- только `defer` (не понимает `async`). -[/smart] - -[warn header="Атрибуты `async/defer` -- только для внешних скриптов"] -Атрибуты `async/defer` работают только в том случае, если назначены на внешние скрипты, т.е. имеющие `src`. - -При попытке назначить их на обычные скрипты <script>...</script>, они будут проигнороированы. -[/warn] - -Тот же пример с `async`: - -```html - -

Важная информация теперь не ждёт, пока загрузится скрипт...

- - - -

...Важная информация!

-``` - -При запуске вы увидите, что вся страница отобразилась тут же, а `alert` из внешнего скрипта появится позже, когда загрузится скрипт. - -[smart header="Эти атрибуты давно \"в ходу\""] -Большинство современных систем рекламы и счётчиков знают про эти атрибуты и используют их. - -Перед вставкой внешнего тега `` -
  • Специальные атрибуты `async` и `defer` используются для того, чтобы пока грузится внешний скрипт -- браузер показал остальную (следующую за ним) часть страницы. Без них этого не происходит.
  • -
  • Разница между `async` и `defer`: атрибут `defer` сохраняет относительную последовательность скриптов, а `async` -- нет. Кроме того, `defer` всегда ждёт, пока весь HTML-документ будет готов, а `async` -- нет.
  • - - -Очень важно не только читать учебник, но делать что-то самостоятельно. - -Решите задачки, чтобы удостовериться, что вы всё правильно поняли. - - diff --git a/1-js/2-first-steps/20-named-function-expression/1-nfe-check/solution.md b/1-js/2-first-steps/20-named-function-expression/1-nfe-check/solution.md deleted file mode 100644 index 5875fae8..00000000 --- a/1-js/2-first-steps/20-named-function-expression/1-nfe-check/solution.md +++ /dev/null @@ -1,21 +0,0 @@ -**Первый код выведет `function ...`, второй -- ошибку во всех браузерах, кроме IE8-.** - -```js -//+ run untrusted no-beautify -// обычное объявление функции (Function Declaration) -function g() { return 1; }; - -alert(g); // функция -``` - -Во втором коде скобки есть, значит функция внутри является не `Function Declaration`, а частью выражения, то есть `Named Function Expression`. Его имя видно только внутри, снаружи переменная `g` не определена. - -```js -//+ run untrusted no-beautify -// Named Function Expression! -(function g() { return 1; }); - -alert(g); // Ошибка! -``` - -Все браузеры, кроме IE8-, поддерживают это ограничение видимости и выведут ошибку, `"undefined variable"`. \ No newline at end of file diff --git a/1-js/2-first-steps/20-named-function-expression/1-nfe-check/task.md b/1-js/2-first-steps/20-named-function-expression/1-nfe-check/task.md deleted file mode 100644 index 0510ff4d..00000000 --- a/1-js/2-first-steps/20-named-function-expression/1-nfe-check/task.md +++ /dev/null @@ -1,22 +0,0 @@ -# Проверка на NFE - -[importance 5] - -Каков будет результат выполнения кода? - -```js -//+ no-beautify -function g() { return 1; } - -alert(g); -``` - -А такого? Будет ли разница, если да -- почему? - -```js -//+ no-beautify -(function g() { return 1; }); - -alert(g); -``` - diff --git a/1-js/2-first-steps/20-named-function-expression/article.md b/1-js/2-first-steps/20-named-function-expression/article.md deleted file mode 100644 index c51478a3..00000000 --- a/1-js/2-first-steps/20-named-function-expression/article.md +++ /dev/null @@ -1,147 +0,0 @@ -# Именованные функциональные выражения - -Специально для работы с рекурсией в JavaScript существует особое расширение функциональных выражений, которое называется "Named Function Expression" (сокращённо NFE) или, по-русски, *"именованное функциональное выражение"*. - -[cut] - - -## Named Function Expression [#functions-nfe] - -Обычное функциональное выражение: -```js -var f = function(...) { /* тело функции */ }; -``` - -Именованное с именем `sayHi`: - -```js -//+ no-beautify -var f = function *!*sayHi*/!*(...) { /* тело функции */ }; -``` - -Что же это за имя, которое идёт в дополнение к `f`, и зачем оно? - -Имя функционального выражения (`sayHi`) имеет особый смысл. Оно доступно только изнутри самой функции. - -Это ограничение видимости входит в стандарт JavaScript и поддерживается всеми браузерами, кроме IE8-. - -Например: - -```js -//+ run -var f = function sayHi(name) { - alert( sayHi ); // изнутри функции - видно (выведет код функции) -}; - -alert( sayHi ); // снаружи - не видно (ошибка: undefined variable 'sayHi') -``` - -Кроме того, имя NFE нельзя перезаписать: - -```js -//+ run -var test = function sayHi(name) { -*!* - sayHi = "тест"; // попытка перезаписи -*/!* - alert( sayHi ); // function... (перезапись не удалась) -}; - -test(); -``` - -В режиме `use strict` код выше выдал бы ошибку. - -Как правило, имя NFE используется для единственной цели -- позволить изнутри функции вызвать саму себя. - -## Пример использования - -NFE используется в первую очередь в тех ситуациях, когда функцию нужно передавать в другое место кода или перемещать из одной переменной в другую. - -**Внутреннее имя позволяет функции надёжно обращаться к самой себе, где бы она ни находилась.** - -Вспомним, к примеру, функцию-факториал из задачи [](/task/factorial): - -```js -//+ run -function f(n) { - return n ? n * f(n - 1) : 1; -}; - -alert( f(5) ); // 120 -``` - -Попробуем перенести её в другую переменную `g`: - -```js -//+ run -function f(n) { - return n ? n * f(n - 1) : 1; -}; - -*!* -var g = f; -f = null; -*/!* - -alert( g(5) ); // запуск функции с новым именем - ошибка при выполнении! -``` - -Ошибка возникла потому что функция из своего кода обращается к своему старому имени `f`. А этой функции уже нет, `f = null`. - -Для того, чтобы функция всегда надёжно работала, объявим её как Named Function Expression: - -```js -//+ run no-beautify -var f = function *!*factorial*/!*(n) { - return n ? n**!*factorial*/!*(n-1) : 1; -}; - -var g = f; // скопировали ссылку на функцию-факториал в g -f = null; - -*!* -alert( g(5) ); // 120, работает! -*/!* -``` - -[warn header="В браузере IE8- создаются две функции"] - -Как мы говорили выше, в браузере IE до 9 версии имя NFE видно везде, что является ошибкой с точки зрения стандарта. - -...Но на самом деле ситуация ещё забавнее. Старый IE создаёт в таких случаях целых две функции: одна записывается в переменную `f`, а вторая -- в переменную `factorial`. - -Например: - -```js -//+ run -var f = function factorial(n) { /*...*/ }; - -// в IE8- false -// в остальных браузерах ошибка, т.к. имя factorial не видно -alert( f === factorial ); -``` - -Все остальные браузеры полностью поддерживают именованные функциональные выражения. -[/warn] - - -[smart header="Устаревшее специальное значение `arguments.callee`"] -Если вы давно работаете с JavaScript, то, возможно, знаете, что раньше для этой цели также служило специальное значение `arguments.callee`. - -Если это название вам ни о чём не говорит -- всё в порядке, читайте дальше, мы обязательно обсудим его [в отдельной главе](#arguments-callee). - -Если же вы в курсе, то стоит иметь в виду, что оно официально исключено из современного стандарта. А NFE -- это наше настоящее. -[/smart] - - -## Итого - -Если функция задана как Function Expression, ей можно дать имя. - -Оно будет доступно только внутри функции (кроме IE8-). - -Это имя предназначено для надёжного рекурсивного вызова функции, даже если она записана в другую переменную. - -Обратим внимание, что с Function Declaration так поступить нельзя. Такое "специальное" внутреннее имя функции задаётся только в синтаксисе Function Expression. - diff --git a/1-js/2-first-steps/21-javascript-specials/article.md b/1-js/2-first-steps/21-javascript-specials/article.md deleted file mode 100644 index 3dad3c2e..00000000 --- a/1-js/2-first-steps/21-javascript-specials/article.md +++ /dev/null @@ -1,400 +0,0 @@ -# Всё вместе: особенности JavaScript - -В этой главе приводятся основные особенности JavaScript, на уровне базовых конструкций, типов, синтаксиса. - -Она будет особенно полезна, если ранее вы программировали на другом языке, ну или как повторение важных моментов раздела. - -Всё очень компактно, со ссылками на развёрнутые описания. - -[cut] - -## Структура кода - -Операторы разделяются точкой с запятой: - -```js -//+ run no-beautify -alert('Привет'); alert('Мир'); -``` - -Как правило, перевод строки тоже подразумевает точку с запятой. Так тоже будет работать: - -```js -//+ run no-beautify -alert('Привет') -alert('Мир') -``` - -...Однако, иногда JavaScript не вставляет точку с запятой. Например: - -```js -//+ run no-beautify -var a = 2 -+3 - -alert(a); // 5 -``` - -Бывают случаи, когда это ведёт к ошибкам, которые достаточно трудно найти и исправить, например: - -```js -//+ run -alert("После этого сообщения будет ошибка") - -[1, 2].forEach(alert) -``` - -Детали того, как работает код выше (массивы `[...]` и `forEach`) мы скоро изучим, здесь важно то, что при установке точки с запятой после `alert` он будет работать корректно. - -**Поэтому в JavaScript рекомендуется точки с запятой ставить. Сейчас это, фактически, общепринятый стандарт.** - -Поддерживаются однострочные комментарии `// ...` и многострочные `/* ... */`: - -Подробнее: [](/structure). - -## Переменные и типы - -
      -
    • Объявляются директивой `var`. Могут хранить любое значение: - -```js -var x = 5; -x = "Петя"; -``` - -
    • -
    • Есть 5 "примитивных" типов и объекты: - -```js -//+ no-beautify -x = 1; // число -x = "Тест"; // строка, кавычки могут быть одинарные или двойные -x = true; // булево значение true/false -x = null; // спец. значение (само себе тип) -x = undefined; // спец. значение (само себе тип) -``` - -Также есть специальные числовые значения `Infinity` (бесконечность) и `NaN`. - -Значение `NaN` обозначает ошибку и является результатом числовой операции, если она некорректна. -
    • -
    • **Значение `null` не является "ссылкой на нулевой адрес/объект" или чем-то подобным. Это просто специальное значение.** - -Оно присваивается, если мы хотим указать, что значение переменной неизвестно. - -Например: - -```js -var age = null; // возраст неизвестен -``` - -
    • -
    • **Значение `undefined` означает "переменная не присвоена".** - -Например: - -```js -var x; -alert( x ); // undefined -``` - -Можно присвоить его и явным образом: `x = undefined`, но так делать не рекомендуется. - -Про объекты мы поговорим в главе [](/object), они в JavaScript сильно отличаются от большинства других языков. -
    • -
    • В имени переменной могут быть использованы любые буквы или цифры, но цифра не может быть первой. Символы доллар `$` и подчёркивание `_` допускаются наравне с буквами.
    • -
    - -Подробнее: [](/variables), [](/types-intro). - -## Строгий режим - -Для того, чтобы интерпретатор работал в режиме максимального соответствия современному стандарту, нужно начинать скрипт директивой `'use strict';` - -```js -'use strict'; - -... -``` - -Эта директива может также указываться в начале функций. При этом функция будет выполняться в режиме соответствия, а на внешний код такая директива не повлияет. - -Одно из важных изменений в современном стандарте -- все переменные нужно объявлять через `var`. Есть и другие, которые мы изучим позже, вместе с соответствующими возможностями языка. - - - -## Взаимодействие с посетителем - -Простейшие функции для взаимодействия с посетителем в браузере: - -
    -
    ["prompt(вопрос[, по_умолчанию])"](https://developer.mozilla.org/en/DOM/window.prompt)
    -
    Задать `вопрос` и возвратить введённую строку, либо `null`, если посетитель нажал "Отмена".
    -
    ["confirm(вопрос)"](https://developer.mozilla.org/en/DOM/window.confirm)
    -
    Задать `вопрос` и предложить кнопки "Ок", "Отмена". Возвращает, соответственно, `true/false`.
    -
    ["alert(сообщение)"](https://developer.mozilla.org/en/DOM/window.alert)
    -
    Вывести сообщение на экран.
    -
    - -Все эти функции являются *модальными*, т.е. не позволяют посетителю взаимодействовать со страницей до ответа. - -Например: - -```js -//+ run -var userName = prompt("Введите имя?", "Василий"); -var isTeaWanted = confirm("Вы хотите чаю?"); - -alert( "Посетитель: " + userName ); -alert( "Чай: " + isTeaWanted ); -``` - -Подробнее: [](/uibasic). - -## Особенности операторов - -
      -
    • **Для сложения строк используется оператор `+`.** - -Если хоть один аргумент -- строка, то другой тоже приводится к строке: - -```js -//+ run -alert( 1 + 2 ); // 3, число -alert( '1' + 2 ); // '12', строка -alert( 1 + '2' ); // '12', строка -``` - -
    • -
    • **Сравнение `===` проверяет точное равенство, включая одинаковый тип.** Это самый очевидный и надёжный способ сравнения. - -**Остальные сравнения `== < <= > >=` осуществляют числовое приведение типа:** - -```js -//+ run -alert( 0 == false ); // true -alert( true > 0 ); // true -``` - -Исключение -- сравнение двух строк (см. далее). - -**Исключение: значения `null` и `undefined` ведут себя в сравнениях не как ноль.** -
        -
      • Они равны `null == undefined` друг другу и не равны ничему ещё. В частности, не равны нулю.
      • -
      • В других сравнениях (кроме `===`) значение `null` преобразуется к нулю, а `undefined` -- становится `NaN` ("ошибка").
      • -
      - -Такое поведение может привести к неочевидным результатам, поэтому лучше всего использовать для сравнения с ними `===`. Оператор `==` тоже можно, если не хотите отличать `null` от `undefined`. - -Например, забавное следствие этих правил для `null`: - -```js -//+ run no-beautify -alert( null > 0 ); // false, т.к. null преобразовано к 0 -alert( null >= 0 ); // true, т.к. null преобразовано к 0 -alert( null == 0 ); // false, в стандарте явно указано, что null равен лишь undefined -``` - -С точки зрения здравого смысла такое невозможно. Значение `null` не равно нулю и не больше, но при этом `null >= 0` возвращает `true`! -
    • - - - -
    • **Сравнение строк -- лексикографическое, символы сравниваются по своим unicode-кодам.** - -Поэтому получается, что строчные буквы всегда больше, чем прописные: - -```js -//+ run -alert( 'а' > 'Я' ); // true -``` - -
    • -
    - -Подробнее: [](/operators), [](/comparison). - -## Логические операторы - -В JavaScript есть логические операторы: И (обозначается `&&`), ИЛИ (обозначается `||`) и НЕ (обозначается `!`). Они интерпретируют любое значение как логическое. - -Не стоит путать их с [побитовыми операторами](/bitwise-operators) И, ИЛИ, НЕ, которые тоже есть в JavaScript и работают с числами на уровне битов. - -Как и в большинстве других языков, в логических операторах используется "короткий цикл" вычислений. Например, вычисление выражения `1 && 0 && 2` остановится после первого И `&&`, т.к. понятно что результат будет ложным (ноль интерпретируется как `false`). - -**Результатом логического оператора служит последнее значение в коротком цикле вычислений.** - -Можно сказать и по-другому: значения хоть и интерпретируются как логические, но то, которое в итоге определяет результат, возвращается без преобразования. - -Например: - -```js -//+ run -alert( 0 && 1 ); // 0 -alert( 1 && 2 && 3 ); // 3 -alert( null || 1 || 2 ); // 1 -``` - -Подробнее: [](/logical-ops). - -## Циклы - -
      -
    • Поддерживаются три вида циклов: - -```js -// 1 -while (условие) { - ... -} - -// 2 -do { - ... -} while (условие); - -// 3 -for (var i = 0; i < 10; i++) { - ... -} -``` - -
    • -
    • Переменную можно объявлять прямо в цикле, но видна она будет и за его пределами.
    • -
    • Поддерживаются директивы `break/continue` для выхода из цикла/перехода на следующую итерацию. - -Для выхода одновременно из нескольких уровней цикла можно задать метку. - -Синтаксис: "`имя_метки:`", ставится она только перед циклами и блоками, например: - -```js -*!*outer:*/!* -for(;;) { - ... - for(;;) { - ... - *!*break outer;*/!* - } -} -``` - -Переход на метку возможен только изнутри цикла, и только на внешний блок по отношению к данному циклу. В произвольное место программы перейти нельзя. -
    • -
    - -Подробнее: [](/while-for). - -## Конструкция switch - -При сравнениях в конструкции `switch` используется оператор `===`. - -Например: - -```js -//+ run -var age = prompt('Ваш возраст', 18); - -switch (age) { - case 18: - alert( 'Никогда не сработает' ); // результат prompt - строка, а не число - - case "18": // вот так - сработает! - alert( 'Вам 18 лет!' ); - break; - - default: - alert( 'Любое значение, не совпавшее с case' ); -} -``` - -Подробнее: [](/switch). - -## Функции - -Синтаксис функций в JavaScript: - -```js -//+ run -// function имя(список параметров) { тело } -function sum(a, b) { - var result = a + b; - - return result; -} - -// использование: -alert( sum(1, 2) ); // 3 -``` - -
      -
    • `sum` -- имя функции, ограничения на имя функции -- те же, что и на имя переменной.
    • -
    • Переменные, объявленные через `var` внутри функции, видны везде внутри этой функции, блоки `if`, `for` и т.п. на видимость не влияют.
    • -
    • Параметры копируются в локальные переменные `a`, `b`. -
    • -
    • Функция без `return` считается возвращающей `undefined`. Вызов `return` без значения также возвращает `undefined`: - -```js -//+ run no-beautify -function f() { } -alert( f() ); // undefined -``` - -
    • -
    - -Подробнее: [](/function-basics). - -## Function Declaration и Expression - -Функция в JavaScript является обычным значением. - -Её можно создать в любом месте кода и присвоить в переменную, вот так: - -```js -//+ run -var sum = function(a, b) { - var result = a + b; - - return result; -} - -alert( sum(1, 2) ); // 3 -``` - -Такой синтаксис, при котором функция объявляется в контексте выражения (в данном случае, выражения присваивания), называется Function Expression, а обычный синтаксис, при котором функция объявляется в основном потоке кода -- Function Declaration. - -Функции, объявленные через Function Declaration, отличаются от Function Expression тем, что интерпретатор создаёт их при входе в область видимости (в начале выполнения скрипта), так что они работают до объявления. - -Обычно это удобно, но может быть проблемой, если нужно объявить функцию в зависимости от условия. В этом случае, а также в других ситуациях, когда хочется создать функцию "здесь и сейчас", используют Function Expression. - -Детали: [](/function-declaration-expression). - -## Named Function Expression - -Если объявление функции является частью какого-либо выражения, например `var f = function...` или любого другого, то это Function Expression. - -В этом случае функции можно присвоить "внутреннее" имя, указав его после `function`. Оно будет видно только внутри этой функции и позволяет обратиться к функции изнутри себя. Обычно это используется для рекурсивных вызовов. - -Например, создадим функцию для вычисления факториала как Function Expression и дадим ей имя `me`: - -```js -//+ run -var factorial = function me(n) { - return (n == 1) ? n : n * me(n - 1); -} - -alert( factorial(5) ); // 120 -*!* -alert( me ); // ошибка, нет такой переменной -*/!* -``` - -Ограничение видимости для имени не работает в IE8-, но вызов с его помощью работает во всех браузерах. - -Более развёрнуто: [](/named-function-expression). - -## Итого - -В этой главе мы повторили основные особенности JavaScript, знание которых необходимо для обхода большинства "граблей", да и просто для написания хорошего кода. - -Это, конечно, лишь основы. Дальше вы узнаете много других особенностей и приёмов программирования на этом языке. diff --git a/1-js/2-first-steps/3-structure/article.md b/1-js/2-first-steps/3-structure/article.md deleted file mode 100644 index ec28445d..00000000 --- a/1-js/2-first-steps/3-structure/article.md +++ /dev/null @@ -1,146 +0,0 @@ -# Структура кода - -В этой главе мы рассмотрим общую структуру кода, команды и их разделение. -[cut] -## Команды - -Раньше мы уже видели пример команды: `alert('Привет, мир!')` выводит сообщение. - -Для того, чтобы добавить в код ещё одну команду -- можно поставить её после точки с запятой. - -Например, вместо одного вызова `alert` сделаем два: - -```js -//+ run no-beautify -alert('Привет'); alert('Мир'); -``` - -Как правило, каждая команда пишется на отдельной строке -- так код лучше читается: - -```js -//+ run no-beautify -alert('Привет'); -alert('Мир'); -``` - -## Точка с запятой [#semicolon] - -Точку с запятой *во многих случаях* можно не ставить, если есть переход на новую строку. - -Так тоже будет работать: - -```js -//+ run no-beautify -alert('Привет') -alert('Мир') -``` - -В этом случае JavaScript интерпретирует переход на новую строчку как разделитель команд и автоматически вставляет "виртуальную" точку с запятой между ними. - -**Однако, важно то, что "во многих случаях" не означает "всегда"!** - -Например, запустите этот код: - -```js -//+ run no-beautify -alert(3 + -1 -+ 2); -``` - -Выведет 6. - -То есть, точка с запятой не ставится. Почему? Интуитивно понятно, что здесь дело в "незавершённом выражении", конца которого JavaScript ждёт с первой строки и поэтому не ставит точку с запятой. И здесь это, пожалуй, хорошо и приятно. - -**Но в некоторых важных ситуациях JavaScript "забывает" вставить точку с запятой там, где она нужна.** - -Таких ситуаций не так много, но ошибки, которые при этом появляются, достаточно сложно обнаруживать и исправлять. - -Чтобы не быть голословным, вот небольшой пример. - -Такой код работает: -```js -//+ run -[1, 2].forEach(alert) -``` - -Он выводит по очереди `1`, `2`. Почему он работает -- сейчас не важно, позже разберёмся. - -Важно, что вот такой код уже работать не будет: - -```js -//+ run no-beautify -alert("Сейчас будет ошибка") -[1, 2].forEach(alert) -``` - -Выведется только первый `alert`, а дальше -- ошибка. Потому что перед квадратной скобкой JavaScript точку с запятой не ставит, а как раз здесь она нужна (упс!). - -Если её поставить, то всё будет в порядке: -```js -//+ run -alert( "Сейчас будет ошибка" ); -[1, 2].forEach(alert) -``` - -**Поэтому в JavaScript рекомендуется точки с запятой ставить. Сейчас это, фактически, стандарт, которому следуют все большие проекты.** - -## Комментарии - -Со временем программа становится большой и сложной. Появляется необходимость добавить *комментарии*, которые объясняют, что происходит и почему. - -Комментарии могут находиться в любом месте программы и никак не влияют на её выполнение. Интерпретатор JavaScript попросту игнорирует их. - -*Однострочные комментарии* начинаются с двойного слэша `//`. Текст считается комментарием до конца строки: - -```js -//+ run -// Команда ниже говорит "Привет" -alert( 'Привет' ); - -alert( 'Мир' ); // Второе сообщение выводим отдельно -``` - -*Многострочные комментарии* начинаются слешем-звездочкой "/*" и заканчиваются звездочкой-слэшем "*/", вот так: - -```js -//+ run -/* Пример с двумя сообщениями. -Это - многострочный комментарий. -*/ -alert( 'Привет' ); -alert( 'Мир' ); -``` - -Всё содержимое комментария игнорируется. Если поместить код внутрь /* ... */ или после `//` -- он не выполнится. - -```js -//+ run -/* Закомментировали код -alert( 'Привет' ); -*/ -alert( 'Мир' ); -``` - -[smart header="Используйте горячие клавиши!"] -В большинстве редакторов комментарий можно поставить горячей клавишей, обычно это [key Ctrl+/] для однострочных и что-то вроде [key Ctrl+Shift+/] -- для многострочных комментариев (нужно выделить блок и нажать сочетание клавиш). Детали смотрите в руководстве по редактору. -[/smart] - -[warn header="Вложенные комментарии не поддерживаются!"] -В этом коде будет ошибка: - -```js -//+ run no-beautify -/* - /* вложенный комментарий ?!? */ -*/ -alert('Мир'); -``` - -[/warn] - - -Не бойтесь комментариев. Чем больше кода в проекте -- тем они важнее. Что же касается увеличения размера кода -- это не страшно, т.к. существуют инструменты сжатия JavaScript, которые при публикации кода легко их удалят. - -На следующих занятиях мы поговорим о переменных, блоках и других структурных элементах программы на JavaScript. - diff --git a/1-js/2-first-steps/4-strict-mode/article.md b/1-js/2-first-steps/4-strict-mode/article.md deleted file mode 100644 index 02670f7b..00000000 --- a/1-js/2-first-steps/4-strict-mode/article.md +++ /dev/null @@ -1,80 +0,0 @@ -# Современный стандарт, "use strict" - -Очень долго язык JavaScript развивался без потери совместимости. Новые возможности добавлялись в язык, но старые -- никогда не менялись, чтобы не "сломать" уже существующие HTML/JS-страницы с их использованием. - -Однако, это привело к тому, что любая ошибка в дизайне языка становилась "вмороженной" в него навсегда. - -Так было до появления стандарта EcmaScript 5 (ES5), который одновременно добавил новые возможности и внёс в язык ряд исправлений, которые могут привести к тому, что старый код, который был написан до его появления, перестанет работать. - -Чтобы этого не случилось, решили, что по умолчанию эти опасные изменения будут выключены, и код будет работать по-старому. А для того, чтобы перевести код в режим полного соответствия современному стандарту, нужно указать специальную директиву `use strict`. - -Эта директива не поддерживается IE9-. - -[cut] - -## Директива use strict - -Директива выглядит как строка `"use strict";` или `'use strict';` и ставится в начале скрипта. - -Например: - -```js -"use strict"; - -// этот код будет работать по современному стандарту ES5 -... -``` - -[warn header="Отменить действие `use strict` никак нельзя"] -Не существует директивы `no use strict` или подобной, которая возвращает в старый режим. - -Если уж вошли в современный режим, то это дорога в один конец. -[/warn] - -[smart header="`use strict` для функций"] -Через некоторое время мы будем проходить [функции](/function-basics). На будущее заметим, что `use strict` также можно указывать в начале функций, тогда строгий режим будет действовать только внутри функции. -[/smart] - -В следующих главах мы будем подробно останавливаться на отличиях в работе языка при `use strict` и без него. - -## Нужен ли мне "use strict"? - -Если говорить абстрактно, то -- да, нужен. В строгом режиме исправлены некоторые ошибки в дизайне языка, и вообще, современный стандарт -- это хорошо. - -Однако, есть и две проблемы. - -
    -
    Поддержка браузеров IE9-, которые игнорируют `"use strict"`.
    -
    Предположим, что мы, используя `"use strict"`, разработали код и протестировали его в браузере Chrome. Всё работает... Однако, вероятность ошибок при этом в IE9- выросла! Он-то всегда работает по старому стандарту, а значит, иногда по-другому. Возникающие ошибки придётся отлаживать уже в IE9-, и это намного менее приятно, нежели в Chrome. - -Впрочем, проблема не так страшна. Несовместимостей мало. И если их знать (а в учебнике мы будем останавливаться на них) и писать правильный код, то всё будет в порядке и `"use strict"` станет нашим верным помощником. -
    -
    Библиотеки, написанные без учёта `"use strict"`.
    -
    Некоторые библиотеки, которые написаны без `"use strict"`, не всегда корректно работают, если вызывающий код содержит `"use strict"`. - -В первую очередь имеются в виду сторонние библиотеки, которые писали не мы, и которые не хотелось бы переписывать или править. - -Таких библиотек мало, но при переводе давно существующих проектов на `"use strict"` эта проблема возникает с завидной регулярностью. -
    -
    - -Вывод? - -**Писать код с `use strict` следует лишь в том случае, если вы уверены, что описанных выше проблем не будет.** - -Конечно же, весь код, который находится в этом учебнике, корректно работает в режиме `"use strict"`. - -## ES5-shim [#es5-shim] - -Браузер IE8 поддерживает только совсем старую версию стандарта JavaScript, а именно ES3. - -К счастью, многие возможности современного стандарта можно добавить в этот браузер, подключив библиотеку [ES5 shim](https://github.com/es-shims/es5-shim), а именно -- скрипты `es5-shim.js` и `es5-sham.js` из неё. - -## Итого - -В этой главе мы познакомились с понятием "строгий режим". - -Далее мы будем предполагать, что разработка ведётся либо в современном браузере, либо в IE8- с подключённым [ES5 shim](https://github.com/es-shims/es5-shim). Это позволит нам использовать большинство возможностей современного JavaScript во всех браузерах. - -Очень скоро, буквально в следующей главе, мы увидим особенности строгого режима на конкретных примерах. - diff --git a/1-js/2-first-steps/5-variables/1-hello-variables/solution.md b/1-js/2-first-steps/5-variables/1-hello-variables/solution.md deleted file mode 100644 index e81aff84..00000000 --- a/1-js/2-first-steps/5-variables/1-hello-variables/solution.md +++ /dev/null @@ -1,13 +0,0 @@ -Каждая строчка решения соответствует одному шагу задачи: - -```js -//+ run -var admin, name; // две переменных через запятую - -name = "Василий"; - -admin = name; - -alert( admin ); // "Василий" -``` - diff --git a/1-js/2-first-steps/5-variables/1-hello-variables/task.md b/1-js/2-first-steps/5-variables/1-hello-variables/task.md deleted file mode 100644 index 42438a4b..00000000 --- a/1-js/2-first-steps/5-variables/1-hello-variables/task.md +++ /dev/null @@ -1,10 +0,0 @@ -# Работа с переменными - -[importance 2] - -
      -
    1. Объявите две переменные: `admin` и `name`.
    2. -
    3. Запишите в `name` строку `"Василий"`.
    4. -
    5. Скопируйте значение из `name` в `admin`.
    6. -
    7. Выведите `admin` (должно вывести "Василий").
    8. -
    \ No newline at end of file diff --git a/1-js/2-first-steps/5-variables/article.md b/1-js/2-first-steps/5-variables/article.md deleted file mode 100644 index 57ce5eff..00000000 --- a/1-js/2-first-steps/5-variables/article.md +++ /dev/null @@ -1,259 +0,0 @@ -# Переменные - -В зависимости от того, для чего вы делаете скрипт, понадобится работать с информацией. - -Если это электронный магазин -- то это товары, корзина. Если чат -- посетители, сообщения и так далее. - -Чтобы хранить информацию, используются *переменные*. -[cut] -## Переменная - -*Переменная* состоит из имени и выделенной области памяти, которая ему соответствует. - -Для *объявления* или, другими словами, *создания переменной* используется ключевое слово `var`: - -```js -var message; -``` - -После объявления, можно записать в переменную данные: - -```js -var message; -message = 'Hello'; // сохраним в переменной строку -``` - -Эти данные будут сохранены в соответствующей области памяти и в дальнейшем доступны при обращении по имени: - -```js -//+ run -var message; -message = 'Hello!'; - -alert( message ); // выведет содержимое переменной -``` - -Для краткости можно совместить объявление переменной и запись данных: - -```js -var message = 'Hello!'; -``` - -Можно даже объявить несколько переменных сразу: - -```js -//+ no-beautify -var user = 'John', age = 25, message = 'Hello'; -``` - -### Аналогия из жизни - -Проще всего понять переменную, если представить её как "коробку" для данных, с уникальным именем. - -Например, переменная `message` -- это коробка, в которой хранится значение `"Hello!"`: - - - -В коробку можно положить любое значение, а позже - поменять его. Значение в переменной можно изменять сколько угодно раз: - -```js -//+ run -var message; - -message = 'Hello!'; - -message = 'World!'; // заменили значение - -alert( message ); -``` - -При изменении значения старое содержимое переменной удаляется. - - - -Можно объявить две переменные и копировать данные из одной в другую: - -```js -//+ run -var hello = 'Hello world!'; - -var message; - -*!* -// скопировали значение -message = hello; -*/!* - -alert( hello ); // Hello world! -alert( message ); // Hello world! -``` - -[smart] -Существуют [функциональные](http://ru.wikipedia.org/wiki/%D0%AF%D0%B7%D1%8B%D0%BA_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B3%D0%BE_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F) языки программирования, в которых значение переменной менять нельзя. Например, [Scala](http://www.scala-lang.org/) или [Erlang](http://www.erlang.org/). - -В таких языках положил один раз значение в коробку -- и оно хранится там вечно, ни удалить ни изменить. А нужно что-то другое сохранить -- изволь создать новую коробку (объявить новую переменную), повторное использование невозможно. - -С виду -- не очень удобно, но, как ни странно, и на таких языках вполне можно успешно программировать. Более того, оказывается, что в ряде областей, например в распараллеливании вычислений, они имеют преимущества. Изучение какого-нибудь функционального языка рекомендуется для расширения кругозора. -[/smart] - -## Имена переменных [#variable-naming] - -На имя переменной в JavaScript наложены всего два ограничения. -
      -
    1. Имя может состоять из: букв, цифр, символов `$` и `_`
    2. -
    3. Первый символ не должен быть цифрой.
    4. -
    - -Например: - -```js -var myName; -var test123; -``` - -**Что особенно интересно -- доллар `'$'` и знак подчеркивания `'_'` являются такими же обычными символами, как буквы:** - -```js -//+ run untrusted -var $ = 1; // объявили переменную с именем '$' -var _ = 2; // переменная с именем '_' - -alert( $ + _ ); // 3 -``` - -А такие переменные были бы неправильными: - -```js -//+ no-beautify -var 1a; // начало не может быть цифрой - -var my-name; // дефис '-' не является разрешенным символом -``` - -[smart header="Регистр букв имеет значение"] -Переменные `apple` и `AppLE` -- две разные переменные. -[/smart] - -[smart header="Русские буквы допустимы, но не рекомендуются"] - -В названии переменных можно использовать и русские буквы, например: - -```js -//+ run -var имя = "Вася"; -alert( имя ); // "Вася" -``` - -Технически, ошибки здесь нет, но на практике сложилась традиция использовать в именах только английские буквы. -[/smart] - -[warn header="Зарезервированные имена"] - -Существует список зарезервированных слов, которые нельзя использовать для переменных, так как они используются самим языком, например: `var, class, return, export` и др. - -Например, такой пример выдаст синтаксическую ошибку: - -```js -//+ run no-beautify -var return = 5; // ошибка -alert(return); -``` - -[/warn] - - -## Важность директивы var - -В старом стандарте JavaScript разрешалось создавать переменную и без `var`, просто присвоив ей значение: - -```js -num = 5; // переменная num будет создана, если ее не было -``` - -В режиме `"use strict"` так делать уже нельзя. - -Следующий код выдаст ошибку: - -```js -//+ run -"use strict"; - -*!* -num = 5; // error: num is not defined -*/!* -``` - -Обратим внимание, директиву `use strict` нужно ставить до кода, иначе она не сработает: - -```js -//+ run -var something; - -"use strict"; // слишком поздно - -*!* -num = 5; // ошибки не будет, так как строгий режим не активирован -*/!* -``` - -[warn header="Ошибка в IE8- без `var`"] -Если же вы собираетесь поддерживать IE8-, то у меня для вас ещё одна причина всегда использовать `var`. - -Следущий документ в IE8- ничего не выведет, будет ошибка: - -```html -
    - -``` - -Это потому, что переменная `test` не объявлена через `var` и совпадает с `id` элемента `
    `. Даже не спрашивайте почему -- это ошибка в браузере IE до версии 9. - -Самое "забавное" то, что такая ошибка присвоения значений будет только в IE8- и только если на странице присутствует элемент с совпадающим с именем `id`. - -Такие ошибки особенно "весело" исправлять и отлаживать. - -Вывод простой -- всегда объявляем переменные через `var`, и сюрпризов не будет. Даже в старых IE. -[/warn] - -## Константы - -*Константа* -- это переменная, которая никогда не меняется. Как правило, их называют большими буквами, через подчёркивание. Например: - -```js -//+ run -var COLOR_RED = "#F00"; -var COLOR_GREEN = "#0F0"; -var COLOR_BLUE = "#00F"; -var COLOR_ORANGE = "#FF7F00"; - -var color = COLOR_ORANGE; -alert( color ); // #FF7F00 -``` - -Технически, константа является обычной переменной, то есть её *можно* изменить. Но мы *договариваемся* этого не делать. - -Зачем нужны константы? Почему бы просто не писать `var color = "#FF7F00"`? - -
      -
    1. Во-первых, константа `COLOR_ORANGE` -- это понятное имя. По присвоению `var color="#FF7F00"` непонятно, что цвет -- оранжевый. Иными словами, константа `COLOR_ORANGE` является "понятным псевдонимом" для значения `#FF7F00`.
    2. -
    3. Во-вторых, опечатка в строке, особенно такой сложной как `#FF7F00`, может быть не замечена, а в имени константы её допустить куда сложнее.
    4. -
    - -**Константы используют вместо строк и цифр, чтобы сделать программу понятнее и избежать ошибок.** - -## Итого - -
      -
    • В JavaScript можно объявлять переменные для хранения данных. Это делается при помощи `var`.
    • -
    • Технически, можно просто записать значение и без объявления переменной, однако по ряду причин это не рекомендуется.
    • -
    • Вместе с объявлением можно сразу присвоить значение: `var x = 10`.
    • -
    • Переменные, которые названы `БОЛЬШИМИ_БУКВАМИ`, являются константами, то есть никогда не меняются. Как правило, они используются для удобства, чтобы было меньше ошибок.
    • -
    - - - diff --git a/1-js/2-first-steps/5-variables/variable-change.png b/1-js/2-first-steps/5-variables/variable-change.png deleted file mode 100644 index 19fa6fbf..00000000 Binary files a/1-js/2-first-steps/5-variables/variable-change.png and /dev/null differ diff --git a/1-js/2-first-steps/5-variables/variable-change@2x.png b/1-js/2-first-steps/5-variables/variable-change@2x.png deleted file mode 100644 index d00fbe15..00000000 Binary files a/1-js/2-first-steps/5-variables/variable-change@2x.png and /dev/null differ diff --git a/1-js/2-first-steps/5-variables/variable.png b/1-js/2-first-steps/5-variables/variable.png deleted file mode 100644 index aecd47ad..00000000 Binary files a/1-js/2-first-steps/5-variables/variable.png and /dev/null differ diff --git a/1-js/2-first-steps/5-variables/variable@2x.png b/1-js/2-first-steps/5-variables/variable@2x.png deleted file mode 100644 index 97d2b79d..00000000 Binary files a/1-js/2-first-steps/5-variables/variable@2x.png and /dev/null differ diff --git a/1-js/2-first-steps/6-variable-names/1-declare-variables/solution.md b/1-js/2-first-steps/6-variable-names/1-declare-variables/solution.md deleted file mode 100644 index 692a5bc7..00000000 --- a/1-js/2-first-steps/6-variable-names/1-declare-variables/solution.md +++ /dev/null @@ -1,13 +0,0 @@ -Каждая строчка решения соответствует одному шагу задачи: - -```js -//+ run -var ourPlanetName = "Земля"; // буквально "название нашей планеты" - -var userName = "Петя"; // "имя посетителя" -``` - -Названия переменных можно бы сократить, например, до `planet` и `name`, но тогда станет менее понятно, о чем речь. - -Насколько допустимы такие сокращения -- зависит от скрипта, его размера и сложности, от того, есть ли другие планеты и пользователи. В общем, лучше не жалеть букв и называть переменные так, чтобы по имени можно было легко понять, что в ней находится, и нельзя было перепутать переменные. - \ No newline at end of file diff --git a/1-js/2-first-steps/6-variable-names/1-declare-variables/task.md b/1-js/2-first-steps/6-variable-names/1-declare-variables/task.md deleted file mode 100644 index 1f0ef92f..00000000 --- a/1-js/2-first-steps/6-variable-names/1-declare-variables/task.md +++ /dev/null @@ -1,8 +0,0 @@ -# Объявление переменных - -[importance 3] - -
      -
    1. Создайте переменную для названия нашей планеты и присвойте ей значение `"Земля"`. *Правильное* имя выберите сами.
    2. -
    3. Создайте переменную для имени посетителя со значением `"Петя"`. Имя также на ваш вкус.
    4. -
    \ No newline at end of file diff --git a/1-js/2-first-steps/6-variable-names/article.md b/1-js/2-first-steps/6-variable-names/article.md deleted file mode 100644 index 202a4f03..00000000 --- a/1-js/2-first-steps/6-variable-names/article.md +++ /dev/null @@ -1,120 +0,0 @@ -# Правильный выбор имени переменной - -Правильный выбор имени переменной -- одна из самых важных и сложных вещей в программировании, которая отличает начинающего от гуру. - -[cut] - -Дело в том, что большинство времени мы тратим не на изначальное написание кода, а на его развитие. - -Возможно, эти слова не очевидны, если вы пока что ничего большого не писали или пишете код "только для чтения" (написал 5 строк, отдал заказчику и забыл). Но чем более серьёзные проекты вы будете делать, тем более актуальны они будут для вас. - -Что такое это "развитие"? Это когда я вчера написал код, а сегодня (или спустя неделю) прихожу и хочу его поменять. Например, вывести сообщение не так, а эдак... Обработать товары по-другому, добавить функционал.. А где у меня там сообщение хранится? А где товар?... - -Гораздо проще найти нужные данные, если они правильно помечены, то есть когда переменная названа *правильно*. - -## Правила именования - -
      -
    • **Правило 1.** - -**Никакого транслита. Только английский.** - -Неприемлемы: - -```js -var moiTovari; -var cena; -var ssilka; -``` - -Подойдут: - -```js -var myGoods; -var price; -var link; -``` - -Чем плох транслит? - -Во-первых, среди разработчиков всего мира принято использовать английский язык для имён переменных. И если ваш код потом попадёт к кому-то другому, например вы будете в команде больше чем из одного человека, то велик шанс, что транслит ему не понравится. - -Во-вторых, русский транслит хуже читается и длиннее, чем названия на английском. - -В-третьих, в проектах вы наверняка будете применять библиотеки, написанные другими людьми. Многое уже готово, в распоряжении современного разработчика есть масса инструментов, все они используют названия переменных и функций на английском языке, и вы, конечно, будете их использовать. А от кода, где транслит перемешан с английским -- волосы могут встать дыбом, и не только на голове. - -Если вы вдруг не знаете английский -- самое время выучить. -
    • -
    • **Правило 2.** - -**Использовать короткие имена только для переменных "местного значения".** - -Называть переменные именами, не несущими смысловой нагрузки, например `a`, `e`, `p`, `mg` -- можно только в том случае, если они используются в небольшом фрагменте кода и их применение очевидно. - -Вообще же, название переменной должно быть понятным. Иногда для этого нужно использовать несколько слов. -
    • -
    • **Правило 3.** - -**Переменные из нескольких слов пишутся `вместеВотТак`.** - -Например: - -```js -var borderLeftWidth; -``` - -Этот способ записи называется "верблюжьей нотацией" или, по-английски, "camelCase". - -Существует альтернативный стандарт, когда несколько слов пишутся через знак подчеркивания `'_'`: - -```js -var border_left_width; -``` - -Преимущественно в JavaScript используется вариант `borderLeftWidth`, в частности во встроенных языковых и браузерных функциях. Поэтому целесообразно остановиться на нём. - -Ещё одна причина выбрать "верблюжью нотацию" -- запись в ней немного короче, чем c подчеркиванием, т.к. не нужно вставлять `'_'`. -
    • -
    • **Правило последнее, главное.** - -**Имя переменной должно максимально чётко соответствовать хранимым в ней данным.** - -Придумывание таких имен -- одновременно коротких и точных, при которых всегда понятно, что где лежит, приходит с опытом, но только если сознательно стремиться к этому. -
    • -
    - -Позвольте поделиться одним небольшим секретом, который очень прост, но позволит улучшить названия переменных и сэкономит время. - -Бывает так, что, написав код, мы через некоторое время к нему возвращаемся, надо что-то поправить. И мы примерно помним, что переменная, в которой хранится нужное вам значение, называется... Ну, скажем, `borderLeftWidth`. Мы ищем в её в коде, не находим, но, разобравшись, обнаруживаем, что на самом деле переменная называлась вот так: `leftBorderWidth`. - -**Если мы ищем переменную с одним именем, а находим -- с другим, то зачастую самый лучший ход -- это *переименовать* переменную, чтобы имя было тем, которое вы искали.** - -То есть, в коде `leftBorderWidth`, а мы её переименуем на ту, которую искали: `borderLeftWidth`. - -Зачем? Дело в том, что в следующий раз, когда вы захотите что-то поправить, то вы будете искать по тому же самому имени. Соответственно, это сэкономит вам время. - -Есть причина и поважнее. Поскольку именно это имя переменной пришло в голову -- скорее всего, оно больше соответствует хранимым там данным, чем то, которое было мы придумали изначально. Исключения бывают, но в любом случае -- такое несовпадение -- это повод задуматься. - -Чтобы удобно переименовывать переменную, нужно использовать [хороший редактор JavaScript](/editor), тогда этот процесс будет очень простым и быстрым. - -[smart header="Если коротко..."] -Смысл имени переменной -- это "имя на коробке", по которому мы сможем максимально быстро находить нужные нам данные. - -**Не нужно бояться переименовывать переменные, если вы придумали имя получше.** - -Современные редакторы позволяют делать это очень удобно и быстро. Это в конечном счете сэкономит вам время. -[/smart] - - -[warn header="Храните в переменной то, что следует"] -Бывают ленивые программисты, которые, вместо того чтобы объявить новую переменную, используют существующую. - -В результате получается, что такая переменная -- как коробка, в которую кидают то одно, то другое, то третье, при этом не меняя название. Что в ней лежит сейчас? А кто его знает... Нужно подойти, проверить. - -Сэкономит такой программист время на объявлении переменной -- потеряет в два раза больше на отладке кода. - -**"Лишняя" переменная -- добро, а не зло.** -[/warn] - - - diff --git a/1-js/2-first-steps/7-types-intro/article.md b/1-js/2-first-steps/7-types-intro/article.md deleted file mode 100644 index 11a98f86..00000000 --- a/1-js/2-first-steps/7-types-intro/article.md +++ /dev/null @@ -1,168 +0,0 @@ -# Шесть типов данных, typeof - -В JavaScript существует несколько основных типов данных. - -В этой главе мы получим о них общее представление, а позже, в соответствующих главах подробно познакомимся с использованием каждого типа в отдельности. - -[cut] - -## Число "number" - -```js -var n = 123; -n = 12.345; -``` - -Единый тип *число* используется как для целых, так и для дробных чисел. - -Существуют специальные числовые значения `Infinity` (бесконечность) и `NaN` (ошибка вычислений). - -Например, бесконечность `Infinity` получается при делении на ноль: - -```js -//+ run -alert( 1 / 0 ); // Infinity -``` - -Ошибка вычислений `NaN` будет результатом некорректной математической операции, например: - -```js -//+ run -alert( "нечисло" * 2 ); // NaN, ошибка -``` - -Эти значения формально принадлежат типу "число", хотя, конечно, числами в их обычном понимании не являются. - -Особенности работы с числами в JavaScript разобраны в главе [](/number). - -## Строка "string" - -```js -var str = "Мама мыла раму"; -str = 'Одинарные кавычки тоже подойдут'; -``` - -**В JavaScript одинарные и двойные кавычки равноправны.** Можно использовать или те или другие. - -[smart header="Тип *символ* не существует, есть только *строка*."] -В некоторых языках программирования есть специальный тип данных для одного символа. Например, в языке С это `char`. В JavaScript есть только тип "строка" `string`. Что, надо сказать, вполне удобно. -[/smart] - -Более подробно со строками мы познакомимся в главе [](/string). - -## Булевый (логический) тип "boolean" - -У него всего два значения: `true` (истина) и `false` (ложь). - -Как правило, такой тип используется для хранения значения типа да/нет, например: - -```js -//+ no-beautify -var checked = true; // поле формы помечено галочкой -checked = false; // поле формы не содержит галочки -``` - -О нём мы поговорим более подробно, когда будем обсуждать логические вычисления и условные операторы. - -## Специальное значение "null" - -Значение `null` не относится ни к одному из типов выше, а образует свой отдельный тип, состоящий из единственного значения `null`: - -```js -var age = null; -``` - -В JavaScript `null` не является "ссылкой на несуществующий объект" или "нулевым указателем", как в некоторых других языках. Это просто специальное значение, которое имеет смысл "ничего" или "значение неизвестно". - -В частности, код выше говорит о том, что возраст `age` неизвестен. - -## Специальное значение "undefined" - -Значение `undefined`, как и `null`, образует свой собственный тип, состоящий из одного этого значения. Оно имеет смысл "значение не присвоено". - -Если переменная объявлена, но в неё ничего не записано, то её значение как раз и есть `undefined`: - -```js -//+ run -var x; -alert( x ); // выведет "undefined" -``` - -Можно присвоить `undefined` и в явном виде, хотя это делается редко: - -```js -//+ run -var x = 123; -x = undefined; - -alert( x ); // "undefined" -``` - -В явном виде `undefined` обычно не присваивают, так как это противоречит его смыслу. Для записи в переменную "пустого" или "неизвестного" значения используется `null`. - -## Объекты "object" - -Первые 5 типов называют *"примитивными"*. - -Особняком стоит шестой тип: *"объекты"*. - -Он используется для коллекций данных и для объявления более сложных сущностей. - -Объявляются объекты при помощи фигурных скобок `{...}`, например: - -```js -var user = { name: "Вася" }; -``` - -Мы подробно разберём способы объявления объектов и, вообще, работу с объектами, позже, в главе [](/object). - -## Оператор typeof [#type-typeof] - -Оператор `typeof` возвращает тип аргумента. - -У него есть два синтаксиса: со скобками и без: -
      -
    1. Синтаксис оператора: `typeof x`.
    2. -
    3. Синтаксис функции: `typeof(x)`.
    4. -
    - -Работают они одинаково, но первый синтаксис короче. - -**Результатом `typeof` является строка, содержащая тип:** - -```js -typeof undefined // "undefined" - -typeof 0 // "number" - -typeof true // "boolean" - -typeof "foo" // "string" - -typeof {} // "object" - -*!* -typeof null // "object" (1) -*/!* - -*!* -typeof function(){} // "function" (2) -*/!* -``` - -Последние две строки помечены, потому что `typeof` ведет себя в них по-особому. - -
      -
    1. Результат `typeof null == "object"` -- это официально признанная ошибка в языке, которая сохраняется для совместимости. На самом деле `null` -- это не объект, а отдельный тип данных.
    2. -
    3. Функции мы пройдём чуть позже. Пока лишь заметим, что функции не являются отдельным базовым типом в JavaScript, а подвидом объектов. Но `typeof` выделяет функции отдельно, возвращая для них `"function"`. На практике это весьма удобно, так как позволяет легко определить функцию.
    4. -
    - -К работе с типами мы также вернёмся более подробно в будущем, после изучения основных структур данных. - -## Итого - -Есть 5 "примитивных" типов: `number`, `string`, `boolean`, `null`, `undefined` и 6-й тип -- объекты `object`. - -Очень скоро мы изучим их во всех деталях. - -Оператор `typeof x` позволяет выяснить, какой тип находится в `x`, возвращая его в виде строки. diff --git a/1-js/2-first-steps/8-operators/1-increment-order/solution.md b/1-js/2-first-steps/8-operators/1-increment-order/solution.md deleted file mode 100644 index 053e8d8d..00000000 --- a/1-js/2-first-steps/8-operators/1-increment-order/solution.md +++ /dev/null @@ -1,23 +0,0 @@ -# Разъяснения - -```js -//+ run no-beautify -var a = 1, b = 1, c, d; - -// префиксная форма сначала увеличивает a до 2, а потом возвращает -c = ++a; alert(c); // 2 - -// постфиксная форма увеличивает, но возвращает старое значение -d = b++; alert(d); // 1 - -// сначала увеличили a до 3, потом использовали в арифметике -c = (2+ ++a); alert(c); // 5 - -// увеличили b до 3, но в этом выражении оставили старое значение -d = (2+ b++); alert(d); // 4 - -// каждую переменную увеличили по 2 раза -alert(a); // 3 -alert(b); // 3 -``` - diff --git a/1-js/2-first-steps/8-operators/1-increment-order/task.md b/1-js/2-first-steps/8-operators/1-increment-order/task.md deleted file mode 100644 index 180d28eb..00000000 --- a/1-js/2-first-steps/8-operators/1-increment-order/task.md +++ /dev/null @@ -1,20 +0,0 @@ -# Инкремент, порядок срабатывания - -[importance 5] - -Посмотрите, понятно ли вам, почему код ниже работает именно так? - -```js -//+ run no-beautify -var a = 1, b = 1, c, d; - -c = ++a; alert(c); // 2 -d = b++; alert(d); // 1 - -c = (2+ ++a); alert(c); // 5 -d = (2+ b++); alert(d); // 4 - -alert(a); // 3 -alert(b); // 3 -``` - diff --git a/1-js/2-first-steps/8-operators/2-assignment-result/solution.md b/1-js/2-first-steps/8-operators/2-assignment-result/solution.md deleted file mode 100644 index c0e69ce4..00000000 --- a/1-js/2-first-steps/8-operators/2-assignment-result/solution.md +++ /dev/null @@ -1,11 +0,0 @@ -Ответ: `x = 5`. - -Оператор присваивания возвращает значение, которое будет записано в переменную, например: - -```js -//+ run -var a = 2; -alert( a *= 2 ); // 4 -``` - -Отсюда `x = 1 + 4 = 5`. \ No newline at end of file diff --git a/1-js/2-first-steps/8-operators/2-assignment-result/task.md b/1-js/2-first-steps/8-operators/2-assignment-result/task.md deleted file mode 100644 index a3c166af..00000000 --- a/1-js/2-first-steps/8-operators/2-assignment-result/task.md +++ /dev/null @@ -1,12 +0,0 @@ -# Результат присваивания - -[importance 3] - -Чему будет равен `x` в примере ниже? - -```js -var a = 2; - -var x = 1 + (a *= 2); -``` - diff --git a/1-js/2-first-steps/8-operators/article.md b/1-js/2-first-steps/8-operators/article.md deleted file mode 100644 index b8b28f3c..00000000 --- a/1-js/2-first-steps/8-operators/article.md +++ /dev/null @@ -1,428 +0,0 @@ -# Основные операторы - -Для работы с переменными, со значениями, JavaScript поддерживает все стандартные операторы, большинство которых есть и в других языках программирования. - -Несколько операторов мы знаем со школы -- это обычные сложение `+`, умножение `*`, вычитание и так далее. - -В этой главе мы сконцентрируемся на операторах, которые в курсе математики не проходят, и на их особенностях в JavaScript. -[cut] - -## Термины: "унарный", "бинарный", "операнд" - -У операторов есть своя терминология, которая используется во всех языках программирования. - -Прежде, чем мы двинемся дальше -- несколько терминов, чтобы понимать, о чём речь. - -
      -
    • *Операнд* -- то, к чему применяется оператор. Например: `5 * 2` -- оператор умножения с левым и правым операндами. Другое название: "аргумент оператора".
    • -
    • *Унарным* называется оператор, который применяется к одному выражению. Например, оператор унарный минус `"-"` меняет знак числа на противоположный: - -```js -//+ run -var x = 1; - -*!* -x = -x; -*/!* -alert( x ); // -1, применили унарный минус -``` - -
    • -
    • *Бинарным* называется оператор, который применяется к двум операндам. Тот же минус существует и в бинарной форме: - -```js -//+ run no-beautify -var x = 1, y = 3; -alert( y - x ); // 2, бинарный минус -``` -
    • -
    - - -## Сложение строк, бинарный + - -Обычно при помощи плюса `'+'` складывают числа. - -Но если бинарный оператор `'+'` применить к строкам, то он их объединяет в одну: - -```js -var a = "моя" + "строка"; -alert( a ); // моястрока -``` - -Иначе говорят, что "плюс производит конкатенацию (сложение) строк". - -**Если хотя бы один аргумент является строкой, то второй будет также преобразован к строке!** - -Причем не важно, справа или слева находится операнд-строка, в любом случае нестроковый аргумент будет преобразован. Например: - -```js -//+ run -alert( '1' + 2 ); // "12" -alert( 2 + '1' ); // "21" -``` - -**Это приведение к строке -- особенность исключительно бинарного оператора `"+"`.** - -Остальные арифметические операторы работают только с числами и всегда приводят аргументы к числу. - -Например: - -```js -//+ run -alert( 2 - '1' ); // 1 -alert( 6 / '2' ); // 3 -``` - - - -### Преобразование к числу, унарный плюс + - -Унарный, то есть применённый к одному значению, плюс ничего не делает с числами: - -```js -//+ run -alert( +1 ); // 1 -alert( +(1 - 2) ); // -1 -``` - -Как видно, плюс ничего не изменил в выражениях. Результат -- такой же, как и без него. - -Тем не менее, он широко применяется, так как его "побочный эффект" -- преобразование значения в число. - -Например, когда мы получаем значения из HTML-полей или от пользователя, то они обычно в форме строк. - -А что, если их нужно, к примеру, сложить? Бинарный плюс сложит их как строки: - -```js -//+ run -var apples = "2"; -var oranges = "3"; - -alert( apples + oranges ); // "23", так как бинарный плюс складывает строки -``` - -Поэтому используем унарный плюс, чтобы преобразовать к числу: - -```js -//+ run -var apples = "2"; -var oranges = "3"; - -alert( +apples + +oranges ); // 5, число, оба операнда предварительно преобразованы в числа -``` - -С точки зрения математики такое изобилие плюсов может показаться странным. С точки зрения программирования -- никаких разночтений: сначала выполнятся унарные плюсы, приведут строки к числам, а затем -- бинарный `'+'` их сложит. - -Почему унарные плюсы выполнились до бинарного сложения? Как мы сейчас увидим, дело в их приоритете. - -## Приоритет - - -В том случае, если в выражении есть несколько операторов -- порядок их выполнения определяется *приоритетом*. - -Из школы мы знаем, что умножение в выражении `2 * 2 + 1` выполнится раньше сложения, т.к. его *приоритет* выше, а скобки явно задают порядок выполнения. Но в JavaScript -- гораздо больше операторов, поэтому существует целая [таблица приоритетов](https://developer.mozilla.org/en/JavaScript/Reference/operators/operator_precedence). - -Она содержит как уже пройденные операторы, так и те, которые мы еще не проходили. В ней каждому оператору задан числовой приоритет. Тот, у кого число больше -- выполнится раньше. Если приоритет одинаковый, то порядок выполнения -- слева направо. - -Отрывок из таблицы: - - - - - - - - - - - - -
    .........
    15унарный плюс`+`
    15унарный минус`-`
    14умножение`*`
    14деление`/`
    13сложение`+`
    13вычитание`-`
    .........
    3присвоение`=`
    .........
    - -Так как "унарный плюс" имеет приоритет `15`, выше, чем `13` у обычного "сложения", то в выражении `+apples + +oranges` сначала сработали плюсы у `apples` и `oranges`, а затем уже обычное сложение. - -## Присваивание - -Обратим внимание, в таблице приоритетов также есть оператор присваивания `=`. - -У него -- один из самых низких приоритетов: `3`. - -Именно поэтому, когда переменную чему-либо присваивают, например, `x = 2 * 2 + 1` сначала выполнится арифметика, а уже затем -- произойдёт присвоение `=`. - -```js -var x = 2 * 2 + 1; - -alert( x ); // 5 -``` - -**Возможно присваивание по цепочке:** - -```js -//+ run -var a, b, c; - -*!* -a = b = c = 2 + 2; -*/!* - -alert( a ); // 4 -alert( b ); // 4 -alert( c ); // 4 -``` - -Такое присваивание работает справа-налево, то есть сначала вычислятся самое правое выражение `2+2`, присвоится в `c`, затем выполнится `b = c` и, наконец, `a = b`. - -[smart header="Оператор `\"=\"` возвращает значение"] -Все операторы возвращают значение. Вызов `x = выражение` не является исключением. - -Он записывает выражение в `x`, а затем возвращает его. Благодаря этому присваивание можно использовать как часть более сложного выражения: - -```js -//+ run -var a = 1; -var b = 2; - -*!* -var c = 3 - (a = b + 1); -*/!* - -alert( a ); // 3 -alert( c ); // 0 -``` - -В примере выше результатом `(a = b + 1)` является значение, которое записывается в `a` (т.е. `3`). Оно используется для вычисления `c`. - -Забавное применение присваивания, не так ли? - -Знать, как это работает -- стоит обязательно, а вот писать самому -- только если вы уверены, что это сделает код более читаемым и понятным. -[/smart] - - -## Взятие остатка % - -Оператор взятия остатка `%` интересен тем, что, несмотря на обозначение, никакого отношения к процентам не имеет. - -Его результат `a % b` -- это остаток от деления `a` на `b`. - -Например: - -```js -//+ run -alert( 5 % 2 ); // 1, остаток от деления 5 на 2 -alert( 8 % 3 ); // 2, остаток от деления 8 на 3 -alert( 6 % 3 ); // 0, остаток от деления 6 на 3 -``` - - -## Инкремент/декремент: ++, -- - -Одной из наиболее частых операций в JavaScript, как и во многих других языках программирования, является увеличение или уменьшение переменной на единицу. - -Для этого существуют даже специальные операторы: -
      -
    • **Инкремент** `++` увеличивает на 1: - -```js -//+ run no-beautify -var i = 2; -i++; // более короткая запись для i = i + 1. -alert(i); // 3 -``` - -
    • -
    • **Декремент** `--` уменьшает на 1: - -```js -//+ run no-beautify -var i = 2; -i--; // более короткая запись для i = i - 1. -alert(i); // 1 -``` - -
    • -
    - -[warn] -Инкремент/декремент можно применить только к переменной. -Код `5++` даст ошибку. -[/warn] - -Вызывать эти операторы можно не только после, но и перед переменной: `i++` (называется "постфиксная форма") или `++i` ("префиксная форма"). - -Обе эти формы записи делают одно и то же: увеличивают на `1`. - -Тем не менее, между ними существует разница. Она видна только в том случае, когда мы хотим не только увеличить/уменьшить переменную, но и использовать результат в том же выражении. - -Например: - -```js -//+ run -var i = 1; -var a = ++i; // (*) - -alert(a); // *!*2*/!* -``` - -В строке `(*)` вызов `++i` увеличит переменную, а *затем* вернёт ее значение в `a`. Так что в `a` попадёт значение `i` *после* увеличения. - -**Постфиксная форма `i++` отличается от префиксной `++i` тем, что возвращает старое значение, бывшее до увеличения.** - -В примере ниже в `a` попадёт старое значение `i`, равное `1`: - -```js -//+ run -var i = 1; -var a = i++; // (*) - -alert(a); // *!*1*/!* -``` - -
      -
    • Если результат оператора не используется, а нужно только увеличить/уменьшить переменную -- без разницы, какую форму использовать: - -```js -//+ run -var i = 0; -i++; -++i; -alert( i ); // 2 -``` - -
    • -
    • Если хочется тут же использовать результат, то нужна префиксная форма: - -```js -//+ run -var i = 0; -alert( ++i ); // 1 -``` - -
    • -
    • Если нужно увеличить, но нужно значение переменной *до увеличения* -- постфиксная форма: - -```js -//+ run -var i = 0; -alert( i++ ); // 0 -``` - -
    • -
    - -[smart header="Инкремент/декремент можно использовать в любых выражениях"] - -При этом он имеет более высокий приоритет и выполняется раньше, чем арифметические операции: - -```js -//+ run -var i = 1; -alert( 2 * ++i ); // 4 -``` - - - -```js -//+ run -var i = 1; -alert( 2 * i++ ); // 2, выполнился раньше но значение вернул старое -``` - -При этом, нужно с осторожностью использовать такую запись, потому что в более длинной строке при быстром "вертикальном" чтении кода легко пропустить такой `i++`, и будет неочевидно, что переменая увеличивается. - -Три строки, по одному действию в каждой -- длиннее, зато нагляднее: - -```js -//+ run -var i = 1; -alert( 2 * i ); -i++; -``` -[/smart] - -## Побитовые операторы - -Побитовые операторы рассматривают аргументы как 32-разрядные целые числа и работают на уровне их внутреннего двоичного представления. - -Эти операторы не являются чем-то специфичным для JavaScript, они поддерживаются в большинстве языков программирования. - -Поддерживаются следующие побитовые операторы: -
      -
    • AND(и) ( `&` )
    • -
    • OR(или) ( `|` )
    • -
    • XOR(побитовое исключающее или) ( `^` )
    • -
    • NOT(не) ( `~` )
    • -
    • LEFT SHIFT(левый сдвиг) ( `<<` )
    • -
    • RIGHT SHIFT(правый сдвиг) ( `>>` )
    • -
    • ZERO-FILL RIGHT SHIFT(правый сдвиг с заполнением нулями) ( `>>>` )
    • -
    - -Они используются редко, поэтому вынесены в отдельную главу [](/bitwise-operators). - - - -## Сокращённая арифметика с присваиванием - -Часто нужно применить оператор к переменной и сохранить результат в ней же, например: - -```js -var n = 2; -n = n + 5; -n = n * 2; -``` - -Эту запись можно укоротить при помощи совмещённых операторов, вот так: - -```js -//+ run -var n = 2; -n += 5; // теперь n=7 (работает как n = n + 5) -n *= 2; // теперь n=14 (работает как n = n * 2) - -alert( n ); // 14 -``` - -Так можно сделать для операторов `+,-,*,/` и бинарных `<<,>>,>>>,&,|,^`. - -Вызов с присваиванием имеет в точности такой же приоритет, как обычное присваивание, то есть выполнится после большинства других операций: - -```js -//+ run -var n = 2; -n *= 3 + 5; - -alert( n ); // 16 (n = 2 * 8) -``` - - -## Оператор запятая - -Один из самых необычных операторов -- запятая `','`. - -Его можно вызвать явным образом, например: - -```js -//+ run -*!* -a = (5, 6); -*/!* - -alert( a ); -``` - -Запятая позволяет перечислять выражения, разделяя их запятой `','`. Каждое из них -- вычисляется и отбрасывается, за исключением последнего, которое возвращается. - -Запятая -- единственный оператор, приоритет которого ниже присваивания. В выражении `a = (5,6)` для явного задания приоритета использованы скобки, иначе оператор `'='` выполнился бы до запятой `','`, получилось бы `(a=5), 6`. - -Зачем же нужен такой странный оператор, который отбрасывает значения всех перечисленных выражений, кроме последнего? - -Обычно он используется в составе более сложных конструкций, чтобы сделать несколько действий в одной строке. Например: - -```js -// три операции в одной строке -for (*!*a = 1, b = 3, c = a*b*/!*; a < 10; a++) { - ... -} -``` - -Такие трюки используются во многих JavaScript-фреймворках для укорачивания кода. diff --git a/1-js/2-first-steps/9-comparison/article.md b/1-js/2-first-steps/9-comparison/article.md deleted file mode 100644 index 2440ef6c..00000000 --- a/1-js/2-first-steps/9-comparison/article.md +++ /dev/null @@ -1,248 +0,0 @@ -# Операторы сравнения и логические значения - -В этом разделе мы познакомимся с операторами сравнения и с логическими значениями, которые такие операторы возвращают. - -[cut] - -Многие операторы сравнения знакомы нам из математики: - -
      -
    • Больше/меньше: a > b, a < b.
    • -
    • Больше/меньше или равно: a >= b, a <= b.
    • -
    • Равно `a == b`. -Для сравнения используется два символа равенства `'='`. Один символ `a = b` означал бы присваивание.
    • -
    • "Не равно". В математике он пишется как , в JavaScript -- знак равенства с восклицательным знаком перед ним !=.
    • -
    - -## Логические значения - -Как и другие операторы, сравнение возвращает значение. Это значение имеет *логический* тип. - -Существует всего два логических значения: -
      -
    • `true` -- имеет смысл "да", "верно", "истина".
    • -
    • `false` -- означает "нет", "неверно", "ложь".
    • -
    - -Например: - -```js -//+ run -alert( 2 > 1 ); // true, верно -alert( 2 == 1 ); // false, неверно -alert( 2 != 1 ); // true -``` - -Логические значения можно использовать и напрямую, присваивать переменным, работать с ними как с любыми другими: - -```js -//+ run -var a = true; // присваивать явно - -var b = 3 > 4; // или как результат сравнения -alert( b ); // false - -alert( a == b ); // (true == false) неверно, выведет false -``` - -## Сравнение строк - -Строки сравниваются побуквенно: - -```js -//+ run -alert( 'Б' > 'А' ); // true -``` - -[warn header="Осторожно, Unicode!"] -Аналогом "алфавита" во внутреннем представлении строк служит кодировка, у каждого символа -- свой номер (код). JavaScript использует кодировку [Unicode](http://ru.wikipedia.org/wiki/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4). - -При этом сравниваются *численные коды символов*. В частности, код у символа `Б` больше, чем у `А`, поэтому и результат сравнения такой. - -**В кодировке Unicode обычно код у строчной буквы больше, чем у прописной.** - -Поэтому регистр имеет значение: - -```js -//+ run -alert( 'а' > 'Я' ); // true, строчные буквы больше прописных -``` - -Для корректного сравнения символы должны быть в одинаковом регистре. -[/warn] - -Если строка состоит из нескольких букв, то сравнение осуществляется как в телефонной книжке или в словаре. Сначала сравниваются первые буквы, потом вторые, и так далее, пока одна не будет больше другой. - -Иными словами, больше -- та строка, которая в телефонной книге была бы на большей странице. - -Например: -
      -
    • Если первая буква первой строки больше -- значит первая строка больше, независимо от остальных символов: - -```js -//+ run -alert( 'Банан' > 'Аят' ); -``` - -
    • -
    • Если одинаковы -- сравнение идёт дальше. Здесь оно дойдёт до третьей буквы: - -```js -//+ run -alert( 'Вася' > 'Ваня' ); // true, т.к. 'с' > 'н' -``` - -
    • -
    • При этом любая буква больше отсутствия буквы: - -```js -//+ run -alert( 'Привет' > 'Прив' ); // true, так как 'е' больше чем "ничего". -``` - -
    • -
    -Такое сравнение называется *лексикографическим*. - - -[warn] -Обычно мы получаем значения от посетителя в виде строк. Например, `prompt` возвращает *строку*, которую ввел посетитель. - -Числа, полученные таким образом, в виде строк сравнивать нельзя, результат будет неверен. Например: - -```js -//+ run -alert( "2" > "14" ); // true, неверно, ведь 2 не больше 14 -``` - -В примере выше `2` оказалось больше `14`, потому что строки сравниваются посимвольно, а первый символ `'2'` больше `'1'`. - -Правильно было бы преобразовать их к числу явным образом. Например, поставив перед ними `+`: - -```js -//+ run -alert( +"2" > +"14" ); // false, теперь правильно -``` - -[/warn] - -## Сравнение разных типов - -При сравнении значений разных типов, используется числовое преобразование. Оно применяется к обоим значениям. - -Например: - -```js -//+ run -alert( '2' > 1 ); // true, сравнивается как 2 > 1 -alert( '01' == 1 ); // true, сравнивается как 1 == 1 -alert( false == 0 ); // true, false становится числом 0 -alert( true == 1 ); // true, так как true становится числом 1. -``` - -Тема преобразований типов будет продолжена далее, в главе [](/types-conversion). - -## Строгое равенство - -В обычном операторе `==` есть "проблема"" -- он не может отличить `0` от `false`: - -```js -//+ run -alert( 0 == false ); // true -``` - -Та же ситуация с пустой строкой: - - -```js -//+ run -alert( '' == false ); // true -``` - -Это естественное следствие того, что операнды разных типов преобразовались к числу. Пустая строка, как и `false`, при преобразовании к числу дают `0`. - -Что же делать, если всё же нужно отличить `0` от `false`? - -**Для проверки равенства без преобразования типов используются операторы строгого равенства `===` (тройное равно) и `!==`.** - -Если тип разный, то они всегда возвращают `false`: - -```js -//+ run -alert( 0 === false ); // false, т.к. типы различны -``` - -Строгое сравнение предпочтительно, если мы хотим быть уверены, что "сюрпризов" не будет. - -## Сравнение с null и undefined - -Проблемы со специальными значениями возможны, когда к переменной применяется операция сравнения `> < <= >=`, а у неё может быть как численное значение, так и `null/undefined`. - -**Интуитивно кажется, что `null/undefined` эквивалентны нулю, но это не так.** - -Они ведут себя по-другому. - -
      -
    1. Значения `null` и `undefined` равны `==` друг другу и не равны чему бы то ни было ещё. -Это жёсткое правило буквально прописано в спецификации языка.
    2. -
    3. При преобразовании в число `null` становится `0`, а `undefined` становится `NaN`.
    4. -
    - -Посмотрим забавные следствия. - -### Некорректный результат сравнения null с 0 -Сравним `null` с нулём: - -```js -//+ run -alert( null > 0 ); // false -alert( null == 0 ); // false -``` - -Итак, мы получили, что `null` не больше и не равен нулю. А теперь... - -```js -//+ run -alert(null >= 0); // *!*true*/!* -``` - -Как такое возможно? Если нечто *"больше или равно нулю"*, то резонно полагать, что оно либо *больше*, либо *равно*. Но здесь это не так. - -Дело в том, что алгоритмы проверки равенства `==` и сравнения `>= > < <=` работают по-разному. - -Сравнение честно приводит к числу, получается ноль. А при проверке равенства значения `null` и `undefined` обрабатываются особым образом: они равны друг другу, но не равны чему-то ещё. - -В результате получается странная с точки зрения здравого смысла ситуация, которую мы видели в примере выше. - -### Несравнимый undefined - -Значение `undefined` вообще нельзя сравнивать: - -```js -//+ run -alert( undefined > 0 ); // false (1) -alert( undefined < 0 ); // false (2) -alert( undefined == 0 ); // false (3) -``` - -
      -
    • Сравнения `(1)` и `(2)` дают `false` потому, что `undefined` при преобразовании к числу даёт `NaN`. А значение `NaN` по стандарту устроено так, что сравнения `==`, `<`, `>`, `<=`, `>=` и даже `===` с ним возвращают `false`.
    • -
    • Проверка равенства `(3)` даёт `false`, потому что в стандарте явно прописано, что `undefined` равно лишь `null` и ничему другому.
    • -
    - - -**Вывод: любые сравнения с `undefined/null`, кроме точного `===`, следует делать с осторожностью.** - -Желательно не использовать сравнения `>= > < <=` с ними, во избежание ошибок в коде. - - -## Итого - -
      -
    • В JavaScript есть логические значения `true` (истина) и `false` (ложь). Операторы сравнения возвращают их.
    • -
    • Строки сравниваются побуквенно.
    • -
    • Значения разных типов приводятся к числу при сравнении, за исключением строгого равенства `===` (`!==`).
    • -
    • Значения `null` и `undefined` равны `==` друг другу и не равны ничему другому. В других сравнениях (с участием `>`,`<`) их лучше не использовать, так как они ведут себя не как `0`.
    • -
    - -Мы ещё вернёмся к теме сравнения позже, когда лучше изучим различные типы данных в JavaScript. diff --git a/1-js/2-first-steps/index.md b/1-js/2-first-steps/index.md deleted file mode 100644 index eee61f1c..00000000 --- a/1-js/2-first-steps/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Основы JavaScript - -Основные кирпичики из которых состоят скрипты. \ No newline at end of file diff --git a/1-js/3-writing-js/1-debugging-chrome/article.md b/1-js/3-writing-js/1-debugging-chrome/article.md deleted file mode 100644 index cce3064d..00000000 --- a/1-js/3-writing-js/1-debugging-chrome/article.md +++ /dev/null @@ -1,259 +0,0 @@ -# Отладка в браузере Chrome - -Перед тем, как двигаться дальше, поговорим об отладке скриптов. - -Все современные браузеры поддерживают для этого "инструменты разработчика". Исправление ошибок с их помощью намного проще и быстрее. - -На текущий момент самые многофункциональные инструменты -- в браузере Chrome. Также очень хорош Firebug (для Firefox). - -[cut] - -## Общий вид панели Sources - -В вашей версии Chrome панель может выглядеть несколько по-иному, но что где находится, должно быть понятно. - -Зайдите на [страницу с примером](debugging/index.html) браузером Chrome. - -Откройте инструменты разработчика: [key F12] или в меню `Инструменты > Инструменты Разработчика`. - -Выберите сверху `Sources`. - - - -Вы видите три зоны: - -
      -
    1. **Зона исходных файлов.** В ней находятся все подключённые к странице файлы, включая JS/CSS. Выберите `pow.js`, если он не выбран.
    2. -
    3. **Зона текста.** В ней находится текст файлов.
    4. -
    5. **Зона информации и контроля.** Мы поговорим о ней позже.
    6. -
    - -Обычно зона исходных файлов при отладке не нужна. Скройте её кнопкой . - -## Общие кнопки управления - - - -Три наиболее часто используемые кнопки управления: -
    -
    Формат
    -
    Нажатие форматирует текст текущего файла, расставляет отступы. Нужна, если вы хотите разобраться в чужом коде, плохо отформатированном или сжатом.
    -
    Консоль
    -
    Очень полезная кнопка, открывает тут же консоль для запуска команд. Можно смотреть код и тут же запускайть функции. Её нажатие можно заменить на клавишу Esc.
    -
    Окно
    -
    Если код очень большой, то можно вынести инструменты разработки вбок или в отдельное окно, зажав эту кнопку и выбрав соответствующий вариант из списка.
    -
    - -## Точки остановки - -Открыли файл `pow.js` во вкладке Sources? Кликните на 6й строке файла `pow.js`, прямо на цифре 6. - -Поздравляю! Вы поставили "точку остановки" или, как чаще говорят, "брейкпойнт". - -Выглядет это должно примерно так: - - - -Слово *Брейкпойнт* (breakpoint) -- часто используемый английский жаргонизм. Это то место в коде, где отладчик будет *автоматически* останавливать выполнение JavaScript, как только оно до него дойдёт. - - -**В остановленном коде можно посмотреть текущие значения переменных, выполнять команды и т.п., в общем -- отлаживать его.** - -Вы можете видеть, что информация о точке остановки появилась справа, в подвкладке Breakpoints. - -Вкладка Breakpoints очень удобна, когда код большой, она позволяет: - -
      -
    • Быстро перейти на место кода, где стоит брейкпойнт кликом на текст.
    • -
    • Временно выключить брейкпойнт кликом на чекбокс.
    • -
    • Быстро удалить брейкпойнт правым кликом на текст и выбором Remove, и так далее.
    • -
    - -[smart header="Дополнительные возможности"] -
      -
    • Остановку можно инициировать и напрямую из кода скрипта, командой `debugger`: - -```js -function pow(x, n) { - ... - debugger; // <-- отладчик остановится тут - ... -} -``` - -
    • -
    • *Правый клик* на номер строки `pow.js` позволит создать условную точку остановки (conditional breakpoint), т.е. задать условие, при котором точка остановки сработает. - -Это удобно, если остановка нужна только при определённом значении переменной или параметра функции. -
    • -
    -[/smart] - -## Остановиться и осмотреться - -Наша функция выполняется сразу при загрузке страницы, так что самый простой способ активировать отладчик JavaScript -- перезагрузить её. Итак, нажимаем [key F5] (Windows, Linux) или [key Cmd+R] (Mac). - -Если вы сделали всё, как описано выше, то выполнение прервётся как раз на 6й строке. - - - -Обратите внимание на информационные вкладки справа (отмечены стрелками). - -В них мы можем посмотреть текущее состояние: -
      -
    1. **`Watch Expressions` -- показывает текущие значения любых выражений.** - -Можно раскрыть эту вкладку, нажать мышью `+` на ней и ввести любое выражение. Отладчик будет отображать его значение на текущий момент, автоматически перевычисляя его при проходе по коду.
    2. -
    3. **`Call Stack` -- стек вызовов, все вложенные вызовы, которые привели к текущему месту кода.** - -На текущий момент видно, отладчик находится в функции `pow` (pow.js, строка 6), вызванной из анонимного кода (index.html, строка 13).
    4. -
    5. **`Scope Variables` -- переменные.** - -На текущий момент строка 6 ещё не выполнилась, поэтому `result` равен `undefined`. - -В `Local` показываются переменные функции: объявленные через `var` и параметры. Вы также можете там видеть ключевое слово `this`, если вы не знаете, что это такое -- ничего страшного, мы это обсудим позже, в следующих главах учебника. - -В `Global` -- глобальные переменные и функции. -
    6. -
    - -## Управление выполнением - -Пришло время, как говорят, "погонять" скрипт и "оттрейсить" (от англ. trace -- отслеживать) его работу. - -Обратим внимание на панель управления справа-сверху, в ней есть 6 кнопок: - -
    -
    -- продолжить выполнение, горячая клавиша [key F8].
    -
    Продолжает выполнения скрипта с текущего момента в обычном режиме. Если скрипт не встретит новых точек остановки, то в отладчик управление больше не вернётся. - -Нажмите на эту кнопку. - -Скрипт продолжится, далее, в 6й строке находится рекурсивный вызов функции `pow`, т.е. управление перейдёт в неё опять (с другими аргументами) и сработает точка остановки, вновь включая отладчик. - -При этом вы увидите, что выполнение стоит на той же строке, но в `Call Stack` появился новый вызов. - -Походите по стеку вверх-вниз -- вы увидите, что действительно аргументы разные. -
    -
    -- сделать шаг, не заходя внутрь функции, горячая клавиша [key F10].
    -
    Выполняет одну команду скрипта. Если в ней есть вызов функции -- то отладчик обходит его стороной, т.е. не переходит на код внутри. - -Эта кнопка очень удобна, если в текущей строке вызывается функция JS-фреймворка или какая-то другая, которая нас ну совсем не интересует. Тогда выполнение продолжится дальше, без захода в эту функцию, что нам и нужно. - -Обратим внимание, в данном случае эта кнопка при нажатии всё-таки перейдёт внутрь вложенного вызова `pow`, так как внутри `pow` находится брейкпойнт, а на включённых брейкпойнтах отладчик останавливается всегда. -
    -
    -- сделать шаг, горячая клавиша [key F11].
    -
    Выполняет одну команду скрипта и переходит к следующей. Если есть вложенный вызов, то заходит внутрь функции. - -Эта кнопка позволяет подробнейшим образом пройтись по очереди по командам скрипта. -
    -
    -- выполнять до выхода из текущей функции, горячая клавиша [key Shift+F11].
    -
    Выполняет команды до завершения текущей функции. - -Эта кнопка очень удобна в случае, если мы нечаянно вошли во вложенный вызов, который нам не интересен -- чтобы быстро из него выйти. -
    -
    -- отключить/включить все точки остановки.
    -
    Эта кнопка никак не двигает нас по коду, она позволяет временно отключить все точки остановки в файле. -
    -
    -- включить/отключить автоматическую остановку при ошибке.
    -
    Эта кнопка -- одна из самых важных. - -Нажмите её несколько раз. В старых версиях Chrome у неё три режима -- нужен фиолетовый, в новых -- два, тогда достаточно синего. - -Когда она включена, то при ошибке в коде он автоматически остановится и мы сможем посмотреть в отладчике текущие значения переменных, при желании выполнить команды и выяснить, как так получилось. -
    -
    - -**Процесс отладки заключается в том, что мы останавливаем скрипт, смотрим, что с переменными, переходим дальше и ищем, где поведение отклоняется от правильного.** - -[smart header="Continue to here"] -Правый клик на номер строки открывает контекстное меню, в котором можно запустить выполнение кода до неё (Continue to here). Это удобно, когда хочется сразу прыгнуть вперёд и breakpoint неохота ставить. -[/smart] - - - -## Консоль - -При отладке, кроме просмотра переменных и передвижения по скрипту, бывает полезно запускать команды JavaScript. Для этого нужна консоль. - -В неё можно перейти, нажав кнопку "Console" вверху-справа, а можно и открыть в дополнение к отладчику, нажав на кнопку или клавишей [key ESC]. - -**Самая любимая команда разработчиков: `console.log(...)`.** - -Она пишет переданные ей аргументы в консоль, например: - -```js -//+ run -// результат будет виден в консоли -for (var i = 0; i < 5; i++) { - console.log("значение", i); -} -``` - -Полную информацию по специальным командам консоли вы можете получить на странице [Chrome Console API](https://developer.chrome.com/devtools/docs/console-api) и [Chrome CommandLine API](https://developer.chrome.com/devtools/docs/commandline-api). Почти все команды также действуют в Firebug (отладчик для браузера Firefox). - -Консоль поддерживают все браузеры, и, хотя IE10- поддерживает далеко не все функции, но `console.log` работает везде. Используйте его для вывода отладочной информации по ходу работы скрипта. - -## Ошибки - -Ошибки JavaScript выводятся в консоли. - -Например, прервите отладку -- для этого достаточно закрыть инструменты разрабтчика -- и откройте [страницу с ошибкой](error/index.html). - -Перейдите во вкладку Console инструментов разработчика ([key Ctrl+Shift+J] / [key Cmd+Shift+J]). - -В консоли вы увидите что-то подобное: - - -Красная строка -- это сообщение об ошибке. - -Если кликнуть на ссылке `pow.js` в консоли, справа в строке с ошибкой, то мы перейдём непосредственно к месту в скрипте, где возникла ошибка. - -Однако почему она возникла? - -Более подробно прояснить произошедшее нам поможет отладчик. Он может "заморозить" выполнение скрипта на момент ошибки и дать нам возможность посмотреть значения переменных и стека на тот момент. - -Для этого: -
      -
    1. Перейдите на вкладку Sources.
    2. -
    3. Включите остановку при ошибке, кликнув на кнопку
    4. -
    5. Перезагрузите страницу.
    6. -
    - -После перезагрузки страницы JavaScript-код запустится снова и отладчик остановит выполнение на строке с ошибкой: - - - -Можно посмотреть значения переменных. Открыть консоль и попробовать запустить что-то в ней. Поставить брейкпойнты раньше по коду и посмотреть, что привело к такой печальной картине, и так далее. - - -## Итого - -Отладчик позволяет: -
      -
    • Останавливаться на отмеченном месте (breakpoint) или по команде `debugger`.
    • -
    • Выполнять код -- по одной строке или до определённого места.
    • -
    • Смотреть переменные, выполнять команды в консоли и т.п.
    • -
    - -В этой главе кратко описаны возможности отладчика Google Chrome, относящиеся именно к работе с кодом. - -Пока что это всё, что нам надо, но, конечно, инструменты разработчика умеют много чего ещё. В частности, вкладка Elements -- позволяет работать со страницей (понадобится позже), Timeline -- смотреть, что именно делает браузер и сколько это у него занимает и т.п. - -Осваивать можно двумя путями: -
      -
    1. [Официальная документация](https://developer.chrome.com/devtools) (на англ.)
    2. -
    3. Кликать в разных местах и смотреть, что получается. Не забывать о клике правой кнопкой мыши.
    4. -
    - -Мы ещё вернёмся к отладчику позже, когда будем работать с HTML. -[head] - -[/head] \ No newline at end of file diff --git a/1-js/3-writing-js/1-debugging-chrome/debugging.view/index.html b/1-js/3-writing-js/1-debugging-chrome/debugging.view/index.html deleted file mode 100755 index 246dd753..00000000 --- a/1-js/3-writing-js/1-debugging-chrome/debugging.view/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - Пример для отладчика. - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/1-debugging-chrome/debugging.view/pow.js b/1-js/3-writing-js/1-debugging-chrome/debugging.view/pow.js deleted file mode 100755 index 8a9e30db..00000000 --- a/1-js/3-writing-js/1-debugging-chrome/debugging.view/pow.js +++ /dev/null @@ -1,8 +0,0 @@ -function pow(x, n) { - if (n == 1) { - return x; - } - - var result = x * pow(x, n - 1); - return result; -} \ No newline at end of file diff --git a/1-js/3-writing-js/1-debugging-chrome/error.view/index.html b/1-js/3-writing-js/1-debugging-chrome/error.view/index.html deleted file mode 100755 index 246dd753..00000000 --- a/1-js/3-writing-js/1-debugging-chrome/error.view/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - Пример для отладчика. - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/1-debugging-chrome/error.view/pow.js b/1-js/3-writing-js/1-debugging-chrome/error.view/pow.js deleted file mode 100755 index 5939b71b..00000000 --- a/1-js/3-writing-js/1-debugging-chrome/error.view/pow.js +++ /dev/null @@ -1,8 +0,0 @@ -function pow(x, n) { - if (n == 1) { - return y; - } - - var result = x * pow(x, n - 1); - return result; -} \ No newline at end of file diff --git a/1-js/3-writing-js/2-coding-style/1-style-errors/solution.md b/1-js/3-writing-js/2-coding-style/1-style-errors/solution.md deleted file mode 100644 index e2473aa4..00000000 --- a/1-js/3-writing-js/2-coding-style/1-style-errors/solution.md +++ /dev/null @@ -1,51 +0,0 @@ -# Ответ - -Вы могли заметить следующие недостатки, сверху-вниз: - -```js -//+ no-beautify -function pow(x,n) // <- отсутствует пробел между аргументами -{ // <- фигурная скобка на отдельной строке - var result=1; // <- нет пробелов вокруг знака = - for(var i=0;i - - - -Не всё здесь однозначно, так что разберём эти правила подробнее. - -### Фигурные скобки - -Пишутся на той же строке, так называемый "египетский" стиль. Перед скобкой -- пробел. - - - - - -Если у вас уже есть опыт в разработке, и вы привыкли делать скобку на отдельной строке -- это тоже вариант. В конце концов, решать вам. Но в большинстве JavaScript-фреймворков стиль именно такой. - -Если условие и код достаточно короткие, например `if (cond) return null`, то запись в одну строку вполне читаема... Но, как правило, отдельная строка всё равно воспринимается лучше. - -### Длина строки - -Максимальную длину строки согласовывают в команде. Как правило, это либо `80`, либо `120` символов, в зависимости от того, какие мониторы у разработчиков. - -Более длинные строки необходимо разбивать для улучшения читаемости. - -### Отступы - -Отступы нужны двух типов: - -
      -
    • **Горизонтальный отступ, при вложенности -- два(или четыре) пробела.** - -Как правило, используются именно пробелы, т.к. они позволяют сделать более гибкие "конфигурации отступов", чем символ "Tab". - -Например, выровнять аргументы относительно открывающей скобки: -```js -//+ no-beautify -show("Строки" + - " выровнены" + - " строго" + - " одна под другой"); -``` -
    • -
    • **Вертикальный отступ, для лучшей разбивки кода -- перевод строки.** - -Используется, чтобы разделить логические блоки внутри одной функции. В примере разделены инициализация переменных, главный цикл и возвращение результата: - -```js -function pow(x, n) { - var result = 1; - // <-- - for (var i = 0; i < n; i++) { - result *= x; - } - // <-- - return result; -} -``` - -Вставляйте дополнительный перевод строки туда, где это сделает код более читаемым. Не должно быть более 9 строк кода подряд без вертикального отступа. -
    • -
    - -### Точка с запятой - -Точки с запятой нужно ставить, даже если их, казалось бы, можно пропустить. - -Есть языки, в которых точка с запятой не обязательна, и её там никто не ставит. В JavaScript перевод строки её заменяет, но лишь частично, поэтому лучше её ставить, как обсуждалось [ранее](#semicolon). - -## Именование - -Общее правило: -
      -
    • Имя переменной -- существительное.
    • -
    • Имя функции -- глагол или начинается с глагола. Бывает, что имена для краткости делают существительными, но глаголы понятнее.
    • -
    - -Для имён используется английский язык (не транслит) и верблюжья нотация. - -Более подробно -- читайте про [имена функций](#function-naming) и [имена переменных](#variable-naming). - -## Уровни вложенности - -Уровней вложенности должно быть немного. - -Например, [проверки в циклах можно делать через "continue"](#continue), чтобы не было дополнительного уровня `if(..) { ... }`: - -Вместо: - -```js -for (var i = 0; i < 10; i++) { - if (i подходит) { - ... // <- уровень вложенности 2 - } -} -``` - -Используйте: - -```js -for (var i = 0; i < 10; i++) { - if (i *!*не*/!* подходит) *!*continue*/!*; - ... // <- уровень вложенности 1 -} -``` - -Аналогичная ситуация -- с `if/else` и `return`. Следующие две конструкции идентичны. - -Первая: - -```js -function isEven(n) { // проверка чётности - if (n % 2 == 0) { - return true; -*!* - } else { - return false; - } -*/!* -} -``` - -Вторая: - -```js -function isEven(n) { // проверка чётности - if (n % 2 == 0) { - return true; - } - -*!* - return false; -*/!* -} -``` - -Если в блоке `if` идёт `return`, то `else` за ним не нужен. - -**Лучше быстро обработать простые случаи, вернуть результат, а дальше разбираться со сложным, без дополнительного уровня вложенности.** - -В случае с функцией `isEven` можно было бы поступить и проще: - -```js -function isEven(n) { // проверка чётности - return !(n % 2); -} -``` - -...Однако, если код `!(n % 2)` для вас менее очевиден чем предыдущий вариант, то стоит использовать предыдущий. - -Главное для нас -- не краткость кода, а его простота и читаемость. Совсем не всегда более короткий код проще для понимания, чем более развёрнутый. - -## Функции = Комментарии - -Функции должны быть небольшими. Если функция большая -- желательно разбить её на несколько. - -Этому правилу бывает сложно следовать, но оно стоит того. При чем же здесь комментарии? - -Вызов отдельной небольшой функции не только легче отлаживать и тестировать -- сам факт его наличия является *отличным комментарием*. - -Сравните, например, две функции `showPrimes(n)` для вывода простых чисел до `n`. - -Первый вариант использует метку: - -```js -function showPrimes(n) { - nextPrime: for (var i = 2; i < n; i++) { - - for (var j = 2; j < i; j++) { - if (i % j == 0) continue nextPrime; - } - - alert( i ); // простое - } -} -``` - -Второй вариант -- дополнительную функцию `isPrime(n)` для проверки на простоту: - -```js -function showPrimes(n) { - - for (var i = 2; i < n; i++) { - *!*if (!isPrime(i)) continue;*/!* - - alert(i); // простое - } -} - -function isPrime(n) { - for (var i = 2; i < n; i++) { - if ( n % i == 0) return false; - } - return true; -} -``` - -Второй вариант проще и понятнее, не правда ли? Вместо участка кода мы видим описание действия, которое там совершается (проверка `isPrime`). - -## Функции -- под кодом - -Есть два способа расположить функции, необходимые для выполнения кода. - -
      -
    1. Функции над кодом, который их использует: - -```js -// *!*объявить функции*/!* -function createElement() { - ... -} - -function setHandler(elem) { - ... -} - -function walkAround() { - ... -} - -// *!*код, использующий функции*/!* -var elem = createElement(); -setHandler(elem); -walkAround(); -``` - -
    2. -
    3. Сначала код, а функции внизу: - -```js -// *!*код, использующий функции*/!* -var elem = createElement(); -setHandler(elem); -walkAround(); - -// --- *!*функции*/!* --- - -function createElement() { - ... -} - -function setHandler(elem) { - ... -} - -function walkAround() { - ... -} -``` - -
    4. -
    - -...На самом деле существует еще третий "стиль", при котором функции хаотично разбросаны по коду, но это ведь не наш метод, да? - -**Как правило, лучше располагать функции под кодом, который их использует.** - -То есть, предпочтителен 2й способ. - -Дело в том, что при чтении такого кода мы хотим знать в первую очередь, *что он делает*, а уже затем *какие функции ему помогают.* Если первым идёт код, то это как раз дает необходимую информацию. Что же касается функций, то вполне возможно нам и не понадобится их читать, особенно если они названы адекватно и то, что они делают, понятно из названия. - -## Плохие комментарии - -В коде нужны комментарии. - -Сразу начну с того, каких комментариев быть почти не должно. - -**Должен быть минимум комментариев, которые отвечают на вопрос "что происходит в коде?"** - -Что интересно, в коде начинающих разработчиков обычно комментариев либо нет, либо они как раз такого типа: "что делается в этих строках". - -Серьёзно, хороший код и так понятен. - -Об этом замечательно выразился Р.Мартин в книге ["Чистый код"](http://www.ozon.ru/context/detail/id/21916535/): "Если вам кажется, что нужно добавить комментарий для улучшения понимания, это значит, что ваш код недостаточно прост, и, может, стоит переписать его". - -Если у вас образовалась длинная "простыня", то, возможно, стоит разбить её на отдельные функции, и тогда из их названий будет понятно, что делает тот или иной фрагмент. - -Да, конечно, бывают сложные алгоритмы, хитрые решения для оптимизации, поэтому нельзя такие комментарии просто запретить. Но перед тем, как писать подобное -- подумайте: "Нельзя ли сделать код понятным и без них?" - -## Хорошие комментарии - - -А какие комментарии полезны и приветствуются? - -
      -
    • **Архитектурный комментарий -- "как оно, вообще, устроено".** - -Какие компоненты есть, какие технологии использованы, поток взаимодействия. О чём и зачем этот скрипт. Взгляд с высоты птичьего полёта. Эти комментарии особенно нужны, если вы не один, а проект большой. - -Для описания архитектуры, кстати, создан специальный язык [UML](http://ru.wikipedia.org/wiki/Unified_Modeling_Language), красивые диаграммы, но можно и без этого. Главное -- чтобы понятно. -
    • -
    • **Справочный комментарий перед функцией -- о том, что именно она делает, какие параметры принимает и что возвращает.** - -Для таких комментариев существует синтаксис [JSDoc](http://en.wikipedia.org/wiki/JSDoc). - -```js -/** - * Возвращает x в степени n, только для натуральных n - * - * @param {number} x Число для возведения в степень. - * @param {number} n Показатель степени, натуральное число. - * @return {number} x в степени n. - */ -function pow(x, n) { - ... -} -``` - -Такие комментарии позволяют сразу понять, что принимает и что делает функция, не вникая в код. - -Кстати, они автоматически обрабатываются многими редакторами, например [Aptana](http://aptana.com) и редакторами от [JetBrains](http://www.jetbrains.com/), которые учитывают их при автодополнении, а также выводят их в автоподсказках при наборе кода. - -Кроме того, есть инструменты, например [JSDoc 3](https://github.com/jsdoc3/jsdoc), которые умеют генерировать по таким комментариям документацию в формате HTML. Более подробную информацию об этом можно также найти на сайте [](http://usejsdoc.org/). -
    • -
    - -**...Но куда более важными могут быть комментарии, которые объясняют не *что*, а *почему* в коде происходит именно это!** - -Как правило, из кода можно понять, что он делает. Бывает, конечно, всякое, но, в конце концов, вы этот код *видите*. Однако гораздо важнее может быть то, чего вы *не видите*! - -*Почему* это сделано именно так? На это сам код ответа не даёт. - -Например: - -
    -
    Есть несколько способов решения задачи. Почему выбран именно этот?
    -
    -Например, пробовали решить задачу по-другому, но не получилось -- напишите об этом. Почему вы выбрали именно этот способ решения? Особенно это важно в тех случаях, когда используется не первый приходящий в голову способ, а какой-то другой. - -Без этого возможна, например, такая ситуация: -
      -
    • Вы открываете код, который был написан какое-то время назад, и видите, что он "неоптимален".
    • -
    • Думаете: "Какой я был дурак", и переписываете под "более очевидный и правильный" вариант.
    • -
    • ...Порыв, конечно, хороший, да только этот вариант вы уже обдумали раньше. И отказались, а почему -- забыли. В процессе переписывания вспомнили, конечно (к счастью), но результат - потеря времени на повторное обдумывание.
    • -
    - -Комментарии, которые объясняют выбор решения, очень важны. Они помогают понять происходящее и предпринять правильные шаги при развитии кода. -
    -
    Какие неочевидные возможности обеспечивает этот код? Где ещё они используются?
    -
    -В хорошем коде должно быть минимум неочевидного. Но там, где это есть -- пожалуйста, комментируйте. -
    -
    - - -[smart header="Комментарии -- это важно"] -Один из показателей хорошего разработчика -- качество комментариев, которые позволяют эффективно поддерживать код, возвращаться к нему после любой паузы и легко вносить изменения. -[/smart] - -## Руководства по стилю - -Когда написанием проекта занимается целая команда, то должен существовать один стандарт кода, описывающий где и когда ставить пробелы, запятые, переносы строк и т.п. - -Сейчас, когда есть столько готовых проектов, нет смысла придумывать целиком своё руководство по стилю. Можно взять уже готовое, и которому, по желанию, всегда можно что-то добавить. - -Большинство есть на английском, сообщите мне, если найдёте хороший перевод: - -
      -
    • [Google JavaScript Style Guide](http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml)
    • -
    • [JQuery Core Style Guidelines](http://docs.jquery.com/JQuery_Core_Style_Guidelines)
    • -
    • [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript)
    • -
    • [Idiomatic.JS](https://github.com/rwldrn/idiomatic.js) (есть [перевод](https://github.com/rwldrn/idiomatic.js/tree/master/translations/ru_RU))
    • -
    • [Dojo Style Guide](http://dojotoolkit.org/community/styleGuide)
    • -
    - -Для того, чтобы начать разработку, вполне хватит элементов стилей, обозначенных в этой главе. В дальнейшем, посмотрев эти руководства, вы можете выработать и свой стиль, но лучше не делать его особенно "уникальным и неповторимым", себе дороже потом будет с людьми сотрудничать. - -## Автоматизированные средства проверки - -Существуют средства, проверяющие стиль кода. - -Самые известные -- это: - -
      -
    • [JSLint](http://www.jslint.com/) -- проверяет код на соответствие [стилю JSLint](http://www.jslint.com/lint.html), в онлайн-интерфейсе вверху можно ввести код, а внизу различные настройки проверки, чтобы сделать её более мягкой.
    • -
    • [JSHint](http://www.jshint.com/) -- вариант JSLint с большим количеством настроек.
    • -
    • [Closure Linter](https://developers.google.com/closure/utilities/) -- проверка на соответствие [Google JavaScript Style Guide](http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml).
    • -
    - -В частности, JSLint и JSHint интегрированы с большинством редакторов, они гибко настраиваются под нужный стиль и совершенно незаметно улучшают разработку, подсказывая, где и что поправить. - -Побочный эффект -- они видят некоторые ошибки, например необъявленные переменные. У меня это обычно результат опечатки, которые таким образом сразу отлавливаются. Очень рекомендую поставить что-то из этого. Я использую [JSHint](http://www.jshint.com/). - -## Итого - -Описанные принципы оформления кода уместны в большинстве проектов. Есть и другие полезные соглашения. - -Следуя (или не следуя) им, необходимо помнить, что любые советы по стилю хороши лишь тогда, когда делают код читаемее, понятнее, проще в поддержке. - diff --git a/1-js/3-writing-js/2-coding-style/code-style.png b/1-js/3-writing-js/2-coding-style/code-style.png deleted file mode 100644 index d41a9883..00000000 Binary files a/1-js/3-writing-js/2-coding-style/code-style.png and /dev/null differ diff --git a/1-js/3-writing-js/2-coding-style/code-style@2x.png b/1-js/3-writing-js/2-coding-style/code-style@2x.png deleted file mode 100644 index acb8ac1b..00000000 Binary files a/1-js/3-writing-js/2-coding-style/code-style@2x.png and /dev/null differ diff --git a/1-js/3-writing-js/2-coding-style/figure-bracket-style.png b/1-js/3-writing-js/2-coding-style/figure-bracket-style.png deleted file mode 100644 index 992d126e..00000000 Binary files a/1-js/3-writing-js/2-coding-style/figure-bracket-style.png and /dev/null differ diff --git a/1-js/3-writing-js/2-coding-style/figure-bracket-style@2x.png b/1-js/3-writing-js/2-coding-style/figure-bracket-style@2x.png deleted file mode 100644 index 7b937e88..00000000 Binary files a/1-js/3-writing-js/2-coding-style/figure-bracket-style@2x.png and /dev/null differ diff --git a/1-js/3-writing-js/3-write-unmain-code/article.md b/1-js/3-writing-js/3-write-unmain-code/article.md deleted file mode 100644 index dd15be50..00000000 --- a/1-js/3-writing-js/3-write-unmain-code/article.md +++ /dev/null @@ -1,301 +0,0 @@ -# Как писать неподдерживаемый код? - -[warn header="Познай свой код"] -Эта статья представляет собой мой вольный перевод [How To Write Unmaintainable Code](http://mindprod.com/jgloss/unmain.html) ("как писать неподдерживаемый код") с дополнениями, актуальными для JavaScript. - -Возможно, в каких-то из этих советов вам даже удастся узнать "этого парня в зеркале". -[/warn] - - -Предлагаю вашему вниманию советы мастеров древности, следование которым создаст дополнительные рабочие места для JavaScript-разработчиков. - -Если вы будете им следовать, то ваш код будет так сложен в поддержке, что у JavaScript'еров, которые придут после вас, даже простейшее изменение займет годы *оплачиваемого* труда! А сложные задачи оплачиваются хорошо, так что они, определённо, скажут вам "Спасибо". - -Более того, *внимательно* следуя этим правилам, вы сохраните и своё рабочее место, так как все будут бояться вашего кода и бежать от него... - -...Впрочем, всему своя мера. При написании такого кода он не должен *выглядеть* сложным в поддержке, код должен *быть* таковым. - -Явно кривой код может написать любой дурак. Это заметят, и вас уволят, а код будет переписан с нуля. Вы не можете такого допустить. Эти советы учитывают такую возможность. Да здравствует дзен. - - -[cut] - -## Соглашения -- по настроению - -[quote author="Сериал \"Симпсоны\", серия Helter Shelter"] -Рабочий-чистильщик осматривает дом:
    -"...Вот только жук у вас необычный...
    -И чтобы с ним справиться, я должен жить как жук, стать жуком, думать как жук."
    -(грызёт стол Симпсонов) -[/quote] - -Чтобы помешать другому программисту исправить ваш код, вы должны понять путь его мыслей. - -Представьте, перед ним -- ваш большой скрипт. И ему нужно поправить его. У него нет ни времени ни желания, чтобы читать его целиком, а тем более -- досконально разбирать. Он хотел бы по-быстрому найти нужное место, сделать изменение и убраться восвояси без появления побочных эффектов. - -Он рассматривает ваш код как бы через трубочку из туалетной бумаги. Это не даёт ему общей картины, он ищет тот небольшой фрагмент, который ему необходимо изменить. По крайней мере, он надеется, что этот фрагмент будет небольшим. - -**На что он попытается опереться в этом поиске -- так это на соглашения, принятые в программировании, об именах переменных, названиях функций и методов...** - -Как затруднить задачу? Можно везде нарушать соглашения -- это помешает ему, но такое могут заметить, и код будет переписан. Как поступил бы ниндзя на вашем месте? - -**...Правильно! Следуйте соглашениям "в общем", но иногда -- нарушайте их.** - -Тщательно разбросанные по коду нарушения соглашений с одной стороны не делают код явно плохим при первом взгляде, а с другой -- имеют в точности тот же, и даже лучший эффект, чем явное неследование им! - -### Пример из jQuery - -[warn header="jQuery / DOM"] -Этот пример требует знаний jQuery/DOM, если пока их у вас нет -- пропустите его, ничего страшного, но обязательно вернитесь к нему позже. Подобное стоит многих часов отладки. -[/warn] -Во фреймворке jQuery есть метод [wrap](http://api.jquery.com/wrap/), который обёртывает один элемент вокруг другого: - -```js -var img = $(''); // создали новые элементы (jQuery-синтаксис) -var div = $('
    '); // и поместили в переменную - -img.wrap(div); // обернуть img в div -div.append(''); -``` - -Результат кода после операции `wrap` -- два элемента, один вложен в другой: - -```html -
    - -
    -``` - -А что же после `append`? - -Можно предположить, что `` добавится в конец `div`, сразу после `img`... Но ничего подобного! - -Искусный ниндзя уже нанёс свой удар и поведение кода стало неправильным, хотя разработчик об этом даже не подозревает. - -Как правило, методы jQuery работают с теми элементами, которые им переданы. Но не здесь! - -Внутри вызова `img.wrap(div)` происходит клонирование `div` и вокруг `img` оборачивается не сам `div`, а его клон. При этом исходная переменная `div` не меняется, в ней как был пустой `div`, так и остался. - -В итоге, после вызова получается два независимых `div'а`: первый содержит `img` (этот неявный клон никуда не присвоен), а второй -- наш `span`. - -Объяснения не очень понятны? Написано что-то странное? Это просто разум, привыкший, что соглашения уважаются, не допускает мысли, что вызов `wrap` -- неявно клонирует элемент. Ведь другие jQuery-методы, кроме `clone` этого не делают. - -Как говорил [Учитель](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BD%D1%84%D1%83%D1%86%D0%B8%D0%B9): "В древности люди учились для того, чтобы совершенствовать себя. Нынче учатся для того, чтобы удивить других". - -## Краткость -- сестра таланта! - -Пишите "как короче", а не как понятнее. "Меньше букв" -- уважительная причина для нарушения любых соглашений. - -Ваш верный помощник -- возможности языка, использованные неочевидным образом. - -Обратите внимание на оператор вопросительный знак `'?'`, например: - -```js -// код из jQuery -i = i ? i < 0 ? Math.max(0, len + i) : i : 0; -``` - -Разработчик, встретивший эту строку и попытавшийся понять, чему же всё-таки равно `i`, скорее всего придёт к вам за разъяснениями. Смело скажите ему, что короче -- это всегда лучше. Посвятите и его в пути ниндзя. Не забудьте вручить [Дао дэ цзин](http://lib.ru/POECHIN/lao1.txt). - -## Именование - -Существенную часть науки о создании неподдерживаемого кода занимает искусство выбора имён. - -### Однобуквенные переменные - -Называйте переменные коротко: `a`, `b` или `c`. - -В этом случае никто не сможет найти её, используя фунцию "Поиск" текстового редактора. - -Более того, даже найдя -- никто не сможет "расшифровать" её и догадаться, что она означает. - -### Не используйте i для цикла - -В тех местах, где однобуквенные переменные общеприняты, например, в счетчике цикла -- ни в коем случае не используйте стандартные названия `i`, `j`, `k`. Где угодно, только не здесь! - -Остановите свой взыскательный взгляд на чём-нибудь более экзотическом. Например, `x` или `y`. - -Эффективность этого подхода особенно заметна, если тело цикла занимает одну-две страницы (чем длиннее -- тем лучше). - -В этом случае заметить, что переменная -- счетчик цикла, без пролистывания вверх, невозможно. - -### Русские слова и сокращения - -Если вам *приходится* использовать длинные, понятные имена переменных -- что поделать.. Но и здесь есть простор для творчества! - -**Назовите переменные "калькой" с русского языка или как-то "улучшите" английское слово.** - -В одном месте напишите `var ssilka`, в другом `var ssylka`, в третьем `var link`, в четвёртом -- `var lnk`... Это действительно великолепно работает и очень креативно! - -Количество ошибок при поддержке такого кода увеличивается во много раз. - -### Будьте абстрактны при выборе имени - -[quote author="Лао-цзы"]Лучший кувшин лепят всю жизнь.
    -Высокая музыка неподвластна слуху.
    -Великий образ не имеет формы.[/quote] - -При выборе имени старайтесь применить максимально абстрактное слово, например `obj`, `data`, `value`, `item`, `elem` и т.п. - -
      -
    • **Идеальное имя для переменной: `data`.** Используйте это имя везде, где можно. В конце концов, каждая переменная содержит *данные*, не правда ли? - -Но что делать, если имя `data` уже занято? Попробуйте `value`, оно не менее универсально. Ведь каждая переменная содержит *значение*. - -Занято и это? Есть и другой вариант. -
    • -
    • **Называйте переменную по типу данных, которые она хранит: `obj`, `num`, `arr`...** - -Насколько это усложнит разработку? Как ни странно, намного! - -Казалось бы, название переменной содержит информацию, говорит о том, что в переменной -- число, объект или массив... С другой стороны, **когда непосвящённый будет разбирать этот код -- он с удивлением обнаружит, что информации нет!** - -Ведь как раз тип легко понять, запустив отладчик и посмотрев, что внутри. Но в чём смысл этой переменной? Что за массив/объект/число в ней хранится? Без долгой медитации над кодом тут не обойтись! -
    • -
    • **Что делать, если и эти имена кончились? Просто добавьте цифру:** `item1, item2, elem5, data1`...
    • -
    - -### Похожие имена - -Только истинно внимательный программист достоин понять ваш код. Но как проверить, достоин ли читающий? - -**Один из способов -- использовать похожие имена переменных, например `data` и `date`.** Бегло прочитать такой код почти невозможно. А уж заметить опечатку и поправить её... Ммммм... Мы здесь надолго, время попить чайку. - -### А.К.Р.О.Н.И.М - -Используйте сокращения, чтобы сделать код короче. - -Например `ie` (Inner Element), `mc` (Money Counter) и другие. Если вы обнаружите, что путаетесь в них сами -- героически страдайте, но не переписывайте код. Вы знали, на что шли. - -### Хитрые синонимы - -[quote author="Конфуций"]Очень трудно найти чёрную кошку в тёмной комнате, особенно когда её там нет.[/quote] - -**Чтобы было не скучно -- используйте *похожие названия* для обозначения *одинаковых действий*.** - -Например, если метод показывает что-то на экране -- начните его название с `display..` (скажем, `displayElement`), а в другом месте объявите аналогичный метод как `show..` (`showFrame`). - -**Как бы намекните этим, что существует тонкое различие между способами показа в этих методах, хотя на самом деле его нет.** - -По возможности, договоритесь с членами своей команды. Если Вася в своих классах использует `display..`, то Валера -- обязательно `render..`, а Петя -- `paint..`. - -**...И напротив, если есть две функции с важными отличиями -- используйте одно и то же слово для их описания!** Например, с `print...` можно начать метод печати на принтере `printPage`, а также -- метод добавления текста на страницу `printText`. - -А теперь, пусть читающий код думает: "Куда же выводит сообщение `printMessage`?". Особый шик -- добавить элемент неожиданности. Пусть `printMessage` выводит не туда, куда все, а в новое окно! - -### Словарь терминов -- это еда! - -Ни в коем случае не поддавайтесь требованиям написать словарь терминов для проекта. Если же он уже есть -- не следуйте ему, а лучше проглотите и скажите, что так и былО! - -Пусть читающий ваш код программист напрасно ищет различия в `helloUser` и `welcomeVisitor` и пытается понять, когда что использовать. Вы-то знаете, что на самом деле различий нет, но искать их можно о-очень долго. - -**Для обозначения посетителя в одном месте используйте `user`, а в другом `visitor`, в третьем -- просто `u`. Выбирайте одно имя или другое, в зависимости от функции и настроения.** - -Это воплотит сразу два ключевых принципа ниндзя-дизайна -- *сокрытие информации* и *подмена понятий*! - -### Повторно используйте имена - -По возможности, повторно используйте имена переменных, функций и свойств. Просто записывайте в них новые значения. - -Добавляйте новое имя только если это абсолютно необходимо. - -В функции старайтесь обойтись только теми переменными, которые были переданы как параметры. - -Это не только затруднит идентификацию того, что *сейчас* находится в переменной, но и сделает почти невозможным поиск места, в котором конкретное значение было присвоено. - -Цель -- максимально усложнить отладку и заставить читающего код программиста построчно анализировать код и конспектировать изменения переменных для каждой ветки исполнения. - -**Продвинутый вариант этого подхода -- незаметно (!) подменить переменную на нечто похожее, например:** - -```js -function ninjaFunction(elem) { - // 20 строк кода, работающего с elem - - elem = elem.cloneNode(true); - - // еще 20 строк кода, работающего с elem -} -``` - -Программист, пожелавший добавить действия с `elem` во вторую часть функции, будет удивлён. Лишь во время отладки, посмотрев весь код, он с удивлением обнаружит, что оказывается имел дело с клоном! - -Регулярные встречи с этим приемом на практике говорят: защититься невозможно. Эффективно даже против опытного ниндзи. - -### Добавляйте подчеркивания - -Добавляйте подчеркивания `_` и `__` к именам переменных. Желательно, чтобы их смысл был известен только вам, а лучше -- вообще без явной причины. - -Этим вы достигните двух целей. Во-первых, код станет длиннее и менее читаемым, а во-вторых, другой программист будет долго искать смысл в подчёркиваниях. Особенно хорошо сработает и внесет сумятицу в его мысли, если в некоторых частях проекта подчеркивания будут, а в некоторых -- нет. - -В процессе развития кода вы, скорее всего, будете путаться и смешивать стили: добавлять имена с подчеркиваниями там, где обычно подчеркиваний нет, и наоборот. Это нормально и полностью соответствует третьей цели -- увеличить количество ошибок при внесении исправлений. - -### Покажите вашу любовь к разработке - -Пусть все видят, какими замечательными сущностями вы оперируете! Имена `superElement`, `megaFrame` и `niceItem` при благоприятном положении звёзд могут привести к просветлению читающего. - -Действительно, с одной стороны, кое-что написано: `super..`, `mega..`, `nice..` С другой -- это не несёт никакой конкретики. Читающий может решить поискать в этом глубинный смысл и замедитировать на часок-другой оплаченного рабочего времени. - -### Перекрывайте внешние переменные - -[quote author="Гуань Инь-цзы"] -Находясь на свету, нельзя ничего увидеть в темноте.
    -Пребывая же в темноте, увидишь все, что находится на свету. -[/quote] - -Почему бы не использовать одинаковые переменные внутри и снаружи функции? Это просто и не требует придумывать новых имён. - -```js -var *!*user*/!* = authenticateUser(); - -function render() { - var *!*user*/!* = anotherValue(); - ... - ...многобукв... - ... - ... // <-- программист захочет внести исправления сюда, и.. - ... -} -``` - -Зашедший в середину метода `render` программист, скорее всего, не заметит, что переменная `user` локально перекрыта и попытается работать с ней, полагая, что это результат `authenticateUser()`... Ловушка захлопнулась! Здравствуй, отладчик. - -## Мощные функции! - -Не ограничивайте действия функции тем, что написано в её названии. Будьте шире. - -Например, функция `validateEmail(email)` может, кроме проверки e-mail на правильность, выводить сообщение об ошибке и просить заново ввести e-mail. - -**Выберите хотя бы пару дополнительных действий, кроме основного назначения функции.** - -Главное -- они должны быть неочевидны из названия функции. Истинный ниндзя-девелопер сделает так, что они будут неочевидны и из кода тоже. - -**Объединение нескольких смежных действий в одну функцию защитит ваш код от повторного использования.** - -Представьте, что другому разработчику нужно только проверить адрес, а сообщение -- не выводить. Ваша функция `validateEmail(email)`, которая делает и то и другое, ему не подойдёт. Работодатель будет вынужден оплатить создание новой. - - -## Внимание.. Сюр-при-из! - -Есть функции, название которых говорит о том, что они ничего не меняют. Например, `isReady`, `checkPermission`, `findTags`... Предполагается, что при вызове они произведут некие вычисления, или найдут и возвратят полезные данные, но при этом их не изменят. В трактатах это называется "отсутствие сторонних эффектов". - -**По-настоящему красивый приём -- делать в таких функциях что-нибудь полезное, заодно с процессом проверки. Что именно -- совершенно неважно.** - -Удивление и ошеломление, которое возникнет у вашего коллеги, когда он увидит, что функция с названием на `is..`, `check..` или `find...` что-то меняет -- несомненно, расширит его границы разумного! - -**Ещё одна вариация такого подхода -- возвращать нестандартное значение.** - -Ведь общеизвестно, что `is..` и `check..` обычно возвращают `true/false`. Продемонстрируйте оригинальное мышление. Пусть вызов `checkPermission` возвращает не результат `true/false`, а объект с результатами проверки! А чего, полезно. - -Те же разработчики, кто попытается написать проверку `if (checkPermission(..))`, будут весьма удивлены результатом. Ответьте им: "надо читать документацию!". И перешлите эту статью. - -## Заключение - -Все советы выше пришли из реального кода... И в том числе от разработчиков с большим опытом. - -Возможно, даже больше вашего, так что не судите опрометчиво ;) - -
      -
    • Следуйте нескольким из них -- и ваш код станет полон сюрпризов.
    • -
    • Следуйте многим -- и ваш код станет истинно вашим, никто не захочет изменять его.
    • -
    • Следуйте всем -- и ваш код станет ценным уроком для молодых разработчиков, ищущих просветления.
    • -
    \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/1-pow-nan-spec/_js.view/solution.js b/1-js/3-writing-js/4-testing/1-pow-nan-spec/_js.view/solution.js deleted file mode 100644 index 7a67dac5..00000000 --- a/1-js/3-writing-js/4-testing/1-pow-nan-spec/_js.view/solution.js +++ /dev/null @@ -1,10 +0,0 @@ -function pow(x, n) { - if (n < 0) return NaN; - if (Math.round(n) != n) return NaN; - - var result = 1; - for (var i = 0; i < n; i++) { - result *= x; - } - return result; -} \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/1-pow-nan-spec/_js.view/source.js b/1-js/3-writing-js/4-testing/1-pow-nan-spec/_js.view/source.js deleted file mode 100644 index 1450a38d..00000000 --- a/1-js/3-writing-js/4-testing/1-pow-nan-spec/_js.view/source.js +++ /dev/null @@ -1,8 +0,0 @@ -/* исправьте этот код */ -function pow(x, n) { - var result = 1; - for (var i = 0; i < n; i++) { - result *= x; - } - return result; -} \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/1-pow-nan-spec/_js.view/test.js b/1-js/3-writing-js/4-testing/1-pow-nan-spec/_js.view/test.js deleted file mode 100644 index 61453678..00000000 --- a/1-js/3-writing-js/4-testing/1-pow-nan-spec/_js.view/test.js +++ /dev/null @@ -1,26 +0,0 @@ -describe("pow", function() { - - describe("возводит x в степень n", function() { - - function makeTest(x) { - var expected = x * x * x; - it("при возведении " + x + " в степень 3 результат: " + expected, function() { - assert.equal(pow(x, 3), expected); - }); - } - - for (var x = 1; x <= 5; x++) { - makeTest(x); - } - - }); - - it("при возведении в отрицательную степень результат NaN", function() { - assert(isNaN(pow(2, -1)), "pow(2, -1) не NaN"); - }); - - it("при возведении в дробную степень результат NaN", function() { - assert(isNaN(pow(2, 1.5)), "pow(2, -1.5) не NaN"); - }); - -}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/1-pow-nan-spec/solution.md b/1-js/3-writing-js/4-testing/1-pow-nan-spec/solution.md deleted file mode 100644 index 2586dfa3..00000000 --- a/1-js/3-writing-js/4-testing/1-pow-nan-spec/solution.md +++ /dev/null @@ -1,17 +0,0 @@ - - -```js -function pow(x, n) { -*!* - if (n < 0) return NaN; - if (Math.round(n) != n) return NaN; -*/!* - - var result = 1; - for (var i = 0; i < n; i++) { - result *= x; - } - return result; -} -``` - diff --git a/1-js/3-writing-js/4-testing/1-pow-nan-spec/task.md b/1-js/3-writing-js/4-testing/1-pow-nan-spec/task.md deleted file mode 100644 index ea90d617..00000000 --- a/1-js/3-writing-js/4-testing/1-pow-nan-spec/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# Сделать pow по спецификации - -[importance 5] - -Исправьте код функции `pow`, чтобы тесты проходили. - -Для этого ниже в задаче вы найдёте ссылку на песочницу. - -Она содержит HTML с тестами. Обратите внимание, что HTML-страница в ней короче той, что обсуждалась в статье [](/testing). Это потому что библиотеки Chai, Mocha и Sinon объединены в один файл: - -```html - -``` - -Этот файл содержит код библиотек, стили, настройки для них и запуск `mocha.run` по окончании загрузки страницы. Если нет элемента с `id="mocha"`, то результаты выводятся в ``. - -Сборка сделана исключительно для более компактного представления задач, без рекомендаций использовать именно её в проектах. diff --git a/1-js/3-writing-js/4-testing/2-pow-test-0/solution.md b/1-js/3-writing-js/4-testing/2-pow-test-0/solution.md deleted file mode 100644 index 39eae1e7..00000000 --- a/1-js/3-writing-js/4-testing/2-pow-test-0/solution.md +++ /dev/null @@ -1,39 +0,0 @@ -Новый тест может быть, к примеру, таким: - -```js -it("любое число в степени 0 равно 1", function() { - assert.equal(pow(123, 0), 1); -}); -``` - -Конечно, желательно проверить на нескольких числах. - -Поэтому лучше будет создать блок `describe`, аналогичный тому, что мы делали для произвольных чисел: - -```js -describe("любое число, кроме нуля, в степени 0 равно 1", function() { - - function makeTest(x) { - it("при возведении " + x + " в степень 0 результат: 1", function() { - assert.equal(pow(x, 0), 1); - }); - } - - for (var x = -5; x <= 5; x += 2) { - makeTest(x); - } - -}); -``` - -И не забудем добавить отдельный тест для нуля: - -```js -//+ no-beautify -... -it("ноль в нулевой степени даёт NaN", function() { - assert( isNaN(pow(0, 0)), "0 в степени 0 не NaN"); -}); -... -``` - diff --git a/1-js/3-writing-js/4-testing/2-pow-test-0/solution.view/index.html b/1-js/3-writing-js/4-testing/2-pow-test-0/solution.view/index.html deleted file mode 100644 index eb1aedbf..00000000 --- a/1-js/3-writing-js/4-testing/2-pow-test-0/solution.view/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/2-pow-test-0/solution.view/test.js b/1-js/3-writing-js/4-testing/2-pow-test-0/solution.view/test.js deleted file mode 100644 index d2b9a771..00000000 --- a/1-js/3-writing-js/4-testing/2-pow-test-0/solution.view/test.js +++ /dev/null @@ -1,44 +0,0 @@ -describe("pow", function() { - - describe("возводит x в степень n", function() { - - function makeTest(x) { - var expected = x * x * x; - it("при возведении " + x + " в степень 3 результат: " + expected, function() { - assert.equal(pow(x, 3), expected); - }); - } - - for (var x = 1; x <= 5; x++) { - makeTest(x); - } - - }); - - it("при возведении в отрицательную степень результат NaN", function() { - assert(isNaN(pow(2, -1)), "pow(2, -1) не NaN"); - }); - - it("при возведении в дробную степень результат NaN", function() { - assert(isNaN(pow(2, 1.5)), "pow(2, -1.5) не NaN"); - }); - - describe("любое число, кроме нуля, в степени 0 равно 1", function() { - - function makeTest(x) { - it("при возведении " + x + " в степень 0 результат: 1", function() { - assert.equal(pow(x, 0), 1); - }); - } - - for (var x = -5; x <= 5; x += 2) { - makeTest(x); - } - - }); - - it("ноль в нулевой степени даёт NaN", function() { - assert(isNaN(pow(0, 0)), "0 в степени 0 не NaN"); - }); - -}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/2-pow-test-0/source.view/index.html b/1-js/3-writing-js/4-testing/2-pow-test-0/source.view/index.html deleted file mode 100644 index 7cc184b0..00000000 --- a/1-js/3-writing-js/4-testing/2-pow-test-0/source.view/index.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/2-pow-test-0/source.view/test.js b/1-js/3-writing-js/4-testing/2-pow-test-0/source.view/test.js deleted file mode 100644 index 61453678..00000000 --- a/1-js/3-writing-js/4-testing/2-pow-test-0/source.view/test.js +++ /dev/null @@ -1,26 +0,0 @@ -describe("pow", function() { - - describe("возводит x в степень n", function() { - - function makeTest(x) { - var expected = x * x * x; - it("при возведении " + x + " в степень 3 результат: " + expected, function() { - assert.equal(pow(x, 3), expected); - }); - } - - for (var x = 1; x <= 5; x++) { - makeTest(x); - } - - }); - - it("при возведении в отрицательную степень результат NaN", function() { - assert(isNaN(pow(2, -1)), "pow(2, -1) не NaN"); - }); - - it("при возведении в дробную степень результат NaN", function() { - assert(isNaN(pow(2, 1.5)), "pow(2, -1.5) не NaN"); - }); - -}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/2-pow-test-0/task.md b/1-js/3-writing-js/4-testing/2-pow-test-0/task.md deleted file mode 100644 index 28291a3a..00000000 --- a/1-js/3-writing-js/4-testing/2-pow-test-0/task.md +++ /dev/null @@ -1,7 +0,0 @@ -# Добавьте тест к задаче - -[importance 5] - -Добавьте к [предыдущей задаче](/task/pow-nan-spec) тесты, которые будут проверять, что любое число, кроме нуля, в нулевой степени равно `1`, а ноль в нулевой степени даёт `NaN` (это математически корректно, результат 00 не определён). - -При необходимости, исправьте реализацию, чтобы тесты проходили без ошибок. \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/3-pow-test-wrong/solution.md b/1-js/3-writing-js/4-testing/3-pow-test-wrong/solution.md deleted file mode 100644 index 97b66bbc..00000000 --- a/1-js/3-writing-js/4-testing/3-pow-test-wrong/solution.md +++ /dev/null @@ -1,27 +0,0 @@ -Этот тест демонстрирует один из соблазнов, которые ожидают начинающего автора тестов. - -Вместо того, чтобы написать три различных теста, он изложил их в виде одного потока вычислений, с несколькими `assert`. - -Иногда так написать легче и проще, однако при ошибке в тесте гораздо менее очевидно, что же пошло не так. - -Если в сложном тесте произошла ошибка где-то посередине потока вычислений, то придётся выяснять, какие конкретно были входные и выходные данные на этот момент, то есть по сути -- отлаживать код самого теста. - -Гораздо лучше будет разбить тест на несколько блоков `it`, с чётко прописанными входными и выходными данными. - -```js -describe("Возводит x в степень n", function() { - it("5 в степени 1 равно 5", function() { - assert.equal(pow(5, 1), 5); - }); - - it("5 в степени 2 равно 25", function() { - assert.equal(pow(5, 2), 25); - }); - - it("5 в степени 3 равно 125", function() { - assert.equal(pow(5, 3), 125); - }); -}); -``` - -Можно использовать цикл для генерации блоков `it`, в этом случае важно, чтобы сам код такого цикла был достаточно простым. Иногда проще записать несколько блоков `it` вручную, как сделано выше, чем "городить огород" из синтаксических конструкций. diff --git a/1-js/3-writing-js/4-testing/3-pow-test-wrong/task.md b/1-js/3-writing-js/4-testing/3-pow-test-wrong/task.md deleted file mode 100644 index 4d656c2f..00000000 --- a/1-js/3-writing-js/4-testing/3-pow-test-wrong/task.md +++ /dev/null @@ -1,22 +0,0 @@ -# Что не так в тесте? - -[importance 5] - -Что не так в этом тесте функции `pow`? - -```js -it("Возводит x в степень n", function() { - var x = 5; - - var result = x; - assert.equal(pow(x, 1), result); - - var result *= x; - assert.equal(pow(x, 2), result); - - var result *= x; - assert.equal(pow(x, 3), result); -}); -``` - -P.S. Синтаксически он верен и работает, но спроектирован неправильно. \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/article.md b/1-js/3-writing-js/4-testing/article.md deleted file mode 100644 index 6c04f26a..00000000 --- a/1-js/3-writing-js/4-testing/article.md +++ /dev/null @@ -1,452 +0,0 @@ -# Автоматические тесты при помощи chai и mocha - -В этой главе мы разберём основы автоматического тестирования. Оно будет применяться далее в задачах, и вообще, входит в "образовательный минимум" программиста. - -[cut] - -## Зачем нужны тесты? - -При написании функции мы обычно представляем, что она должна делать, какое значение -- на каких аргументах выдавать. - -В процессе разработки мы, время от времени, проверяем, правильно ли работает функция. Самый простой способ проверить -- это запустить её, например, в консоли, и посмотреть результат. - -Если что-то не так -- поправить, опять запустить -- посмотреть результат... И так -- "до победного конца". - -Но такие ручные запуски -- очень несовершенное средство проверки. - -**Когда проверяешь работу кода вручную -- легко его "недотестировать".** - -Например, пишем функцию `f`. Написали, тестируем с разными аргументами. Вызов функции `f(a)` -- работает, а вот `f(b)` -- не работает. Поправили код -- стало работать `f(b)`, вроде закончили. Но при этом забыли заново протестировать `f(a)` -- упс, вот и возможная ошибка в коде. - -**Автоматизированное тестирование -- это когда тесты написаны отдельно от кода, и можно в любой момент запустить их и проверить все важные случаи использования.** - -## BDD -- поведенческие тесты кода - -Мы рассмотрим методику тестирования, которая входит в [BDD](http://en.wikipedia.org/wiki/Behavior-driven_development) -- Behavior Driven Development. Подход BDD давно и с успехом используется во многих проектах. - -BDD -- это не просто тесты. Это гораздо больше. - -**Тесты BDD -- это три в одном: И тесты И документация И примеры использования одновременно.** - -Впрочем, хватит слов. Рассмотрим примеры. - -## Разработка pow: спецификация - -Допустим, мы хотим разработать функцию `pow(x, n)`, которая возводит `x` в целую степень `n`, для простоты `n≥0`. - - -Ещё до разработки мы можем представить себе, что эта функция будет делать и описать это по методике BDD. - -Это описание называется *спецификация* (или, как говорят в обиходе, "спека") и выглядит так: - -```js -describe("pow", function() { - - it("возводит в n-ю степень", function() { - assert.equal(pow(2, 3), 8); - }); - -}); -``` - -У спецификации есть три основных строительных блока, которые вы видите в примере выше: -
    -
    `describe(название, function() { ... })`
    -
    Задаёт, что именно мы описываем, используется для группировки "рабочих лошадок" -- блоков `it`. В данном случае мы описываем функцию `pow`.
    -
    `it(название, function() { ... })`
    -
    В названии блока `it` *человеческим языком* описывается, что должна делать функция, далее следует *тест*, который проверяет это.
    -
    `assert.equal(value1, value2)`
    -
    Код внутри `it`, если реализация верна, должен выполняться без ошибок. - -Различные функции вида `assert.*` используются, чтобы проверить, делает ли `pow` то, что задумано. Пока что нас интересует только одна из них -- `assert.equal`, она сравнивает свой первый аргумент со вторым и выдаёт ошибку в случае, когда они не равны. В данном случае она проверяет, что результат `pow(2, 3)` равен `8`. - - -Есть и другие виды сравнений и проверок, которые мы увидим далее.
    -
    - - -## Поток разработки - -Как правило, поток разработки таков: -
      -
    1. Пишется спецификация, которая описывает самый базовый функционал.
    2. -
    3. Делается начальная реализация.
    4. -
    5. Для проверки соответствия спецификации мы задействуем одновременно фреймворк, в нашем случае [Mocha](http://mochajs.org/) вместе со спецификацией и реализацией. Фреймворк запускает все тесты `it` и выводит ошибки, если они возникнут. При ошибках вносятся исправления.
    6. -
    7. Спецификация расширяется, в неё добавляются возможности, которые пока, возможно, не поддерживаются реализацией.
    8. -
    9. Идём на пункт 3, делаем реализацию, и так далее, до победного конца.
    10. -
    - -Разработка ведётся *итеративно*, один проход за другим, пока спецификация и реализация не будут завершены. - -В нашем случае первый шаг уже завершён, начальная спецификация готова, хорошо бы приступить к реализации. Но перед этим проведём "нулевой" запуск спецификации, просто чтобы увидеть, что уже в таком виде, даже без реализации -- тесты работают. - -## Пример в действии - -Для запуска тестов нужны соответствующие JavaScript-библиотеки. - -Мы будем использовать: -
      -
    • [Mocha](http://mochajs.org/) -- эта библиотека содержит общие функции для тестирования, включая `describe` и `it`.
    • -
    • [Chai](http://chaijs.com) -- библиотека поддерживает разнообразные функции для проверок. Есть разные "стили" проверки результатов, с которыми мы познакомимся позже, на текущий момент мы будем использовать лишь `assert.equal`.
    • -
    • [Sinon](http://sinonjs.org/) -- для эмуляции и хитрой подмены функций "заглушками", понадобится позднее.
    • -
    - -Эти библиотеки позволяют тестировать JS не только в браузере, но и на сервере Node.JS. Здесь мы рассмотрим браузерный вариант, серверный использует те же функции. - -Пример HTML-страницы для тестов: - -```html - -``` - -Эту страницу можно условно разделить на четыре части: -
      -
    1. Блок `` -- в нём мы подключаем библиотеки и стили для тестирования, нашего кода там нет.
    2. -
    3. Блок ` - - - - - - - - - - - -
      - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/beforeafter.view/test.js b/1-js/3-writing-js/4-testing/beforeafter.view/test.js deleted file mode 100755 index e9637032..00000000 --- a/1-js/3-writing-js/4-testing/beforeafter.view/test.js +++ /dev/null @@ -1,24 +0,0 @@ -describe("Тест", function() { - - before(function() { - alert("Начало всех тестов"); - }); - after(function() { - alert("Окончание всех тестов"); - }); - - beforeEach(function() { - alert("Вход в тест"); - }); - afterEach(function() { - alert("Выход из теста"); - }); - - it('тест 1', function() { - alert('1'); - }); - it('тест 2', function() { - alert('2'); - }); - -}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/index.html b/1-js/3-writing-js/4-testing/index.html deleted file mode 100755 index dac2c912..00000000 --- a/1-js/3-writing-js/4-testing/index.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-1.view/index.html b/1-js/3-writing-js/4-testing/pow-1.view/index.html deleted file mode 100755 index dac2c912..00000000 --- a/1-js/3-writing-js/4-testing/pow-1.view/index.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-1.view/test.js b/1-js/3-writing-js/4-testing/pow-1.view/test.js deleted file mode 100755 index 582287f9..00000000 --- a/1-js/3-writing-js/4-testing/pow-1.view/test.js +++ /dev/null @@ -1,7 +0,0 @@ -describe("pow", function() { - - it("возводит в n-ю степень", function() { - assert.equal(pow(2, 3), 8); - }); - -}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-2.view/index.html b/1-js/3-writing-js/4-testing/pow-2.view/index.html deleted file mode 100755 index 57aebaf7..00000000 --- a/1-js/3-writing-js/4-testing/pow-2.view/index.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-2.view/test.js b/1-js/3-writing-js/4-testing/pow-2.view/test.js deleted file mode 100755 index a14e3afa..00000000 --- a/1-js/3-writing-js/4-testing/pow-2.view/test.js +++ /dev/null @@ -1,11 +0,0 @@ -describe("pow", function() { - - it("при возведении 2 в 3ю степень результат 8", function() { - assert.equal(pow(2, 3), 8); - }); - - it("при возведении 3 в 4ю степень равен 81", function() { - assert.equal(pow(3, 4), 81); - }); - -}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-3.view/index.html b/1-js/3-writing-js/4-testing/pow-3.view/index.html deleted file mode 100755 index 68d14a3f..00000000 --- a/1-js/3-writing-js/4-testing/pow-3.view/index.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-3.view/test.js b/1-js/3-writing-js/4-testing/pow-3.view/test.js deleted file mode 100755 index 43457d87..00000000 --- a/1-js/3-writing-js/4-testing/pow-3.view/test.js +++ /dev/null @@ -1,14 +0,0 @@ -describe("pow", function() { - - function makeTest(x) { - var expected = x * x * x; - it("при возведении " + x + " в степень 3 результат: " + expected, function() { - assert.equal(pow(x, 3), expected); - }); - } - - for (var x = 1; x <= 5; x++) { - makeTest(x); - } - -}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-4.view/index.html b/1-js/3-writing-js/4-testing/pow-4.view/index.html deleted file mode 100755 index 68d14a3f..00000000 --- a/1-js/3-writing-js/4-testing/pow-4.view/index.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-4.view/test.js b/1-js/3-writing-js/4-testing/pow-4.view/test.js deleted file mode 100755 index 0e03f0fe..00000000 --- a/1-js/3-writing-js/4-testing/pow-4.view/test.js +++ /dev/null @@ -1,20 +0,0 @@ -describe("pow", function() { - - describe("возводит x в степень n", function() { - - function makeTest(x) { - var expected = x * x * x; - it("при возведении " + x + " в степень 3 результат: " + expected, function() { - assert.equal(pow(x, 3), expected); - }); - } - - for (var x = 1; x <= 5; x++) { - makeTest(x); - } - - }); - - // ... - -}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-full.view/index.html b/1-js/3-writing-js/4-testing/pow-full.view/index.html deleted file mode 100755 index a0f4faaf..00000000 --- a/1-js/3-writing-js/4-testing/pow-full.view/index.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-full.view/test.js b/1-js/3-writing-js/4-testing/pow-full.view/test.js deleted file mode 100755 index d2b9a771..00000000 --- a/1-js/3-writing-js/4-testing/pow-full.view/test.js +++ /dev/null @@ -1,44 +0,0 @@ -describe("pow", function() { - - describe("возводит x в степень n", function() { - - function makeTest(x) { - var expected = x * x * x; - it("при возведении " + x + " в степень 3 результат: " + expected, function() { - assert.equal(pow(x, 3), expected); - }); - } - - for (var x = 1; x <= 5; x++) { - makeTest(x); - } - - }); - - it("при возведении в отрицательную степень результат NaN", function() { - assert(isNaN(pow(2, -1)), "pow(2, -1) не NaN"); - }); - - it("при возведении в дробную степень результат NaN", function() { - assert(isNaN(pow(2, 1.5)), "pow(2, -1.5) не NaN"); - }); - - describe("любое число, кроме нуля, в степени 0 равно 1", function() { - - function makeTest(x) { - it("при возведении " + x + " в степень 0 результат: 1", function() { - assert.equal(pow(x, 0), 1); - }); - } - - for (var x = -5; x <= 5; x += 2) { - makeTest(x); - } - - }); - - it("ноль в нулевой степени даёт NaN", function() { - assert(isNaN(pow(0, 0)), "0 в степени 0 не NaN"); - }); - -}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-min.view/index.html b/1-js/3-writing-js/4-testing/pow-min.view/index.html deleted file mode 100755 index 57aebaf7..00000000 --- a/1-js/3-writing-js/4-testing/pow-min.view/index.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-min.view/test.js b/1-js/3-writing-js/4-testing/pow-min.view/test.js deleted file mode 100755 index 582287f9..00000000 --- a/1-js/3-writing-js/4-testing/pow-min.view/test.js +++ /dev/null @@ -1,7 +0,0 @@ -describe("pow", function() { - - it("возводит в n-ю степень", function() { - assert.equal(pow(2, 3), 8); - }); - -}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-nan-assert.view/index.html b/1-js/3-writing-js/4-testing/pow-nan-assert.view/index.html deleted file mode 100755 index 5c0dd157..00000000 --- a/1-js/3-writing-js/4-testing/pow-nan-assert.view/index.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-nan-assert.view/test.js b/1-js/3-writing-js/4-testing/pow-nan-assert.view/test.js deleted file mode 100755 index 61453678..00000000 --- a/1-js/3-writing-js/4-testing/pow-nan-assert.view/test.js +++ /dev/null @@ -1,26 +0,0 @@ -describe("pow", function() { - - describe("возводит x в степень n", function() { - - function makeTest(x) { - var expected = x * x * x; - it("при возведении " + x + " в степень 3 результат: " + expected, function() { - assert.equal(pow(x, 3), expected); - }); - } - - for (var x = 1; x <= 5; x++) { - makeTest(x); - } - - }); - - it("при возведении в отрицательную степень результат NaN", function() { - assert(isNaN(pow(2, -1)), "pow(2, -1) не NaN"); - }); - - it("при возведении в дробную степень результат NaN", function() { - assert(isNaN(pow(2, 1.5)), "pow(2, -1.5) не NaN"); - }); - -}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-nan.view/index.html b/1-js/3-writing-js/4-testing/pow-nan.view/index.html deleted file mode 100755 index 5c0dd157..00000000 --- a/1-js/3-writing-js/4-testing/pow-nan.view/index.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      - - - - - - \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-nan.view/test.js b/1-js/3-writing-js/4-testing/pow-nan.view/test.js deleted file mode 100755 index a0c37e0b..00000000 --- a/1-js/3-writing-js/4-testing/pow-nan.view/test.js +++ /dev/null @@ -1,26 +0,0 @@ -describe("pow", function() { - - describe("возводит x в степень n", function() { - - function makeTest(x) { - var expected = x * x * x; - it("при возведении " + x + " в степень 3 результат: " + expected, function() { - assert.equal(pow(x, 3), expected); - }); - } - - for (var x = 1; x <= 5; x++) { - makeTest(x); - } - - }); - - it("при возведении в отрицательную степень результат NaN", function() { - assert(isNaN(pow(2, -1))); - }); - - it("при возведении в дробную степень результат NaN", function() { - assert(isNaN(pow(2, 1.5))); - }); - -}); \ No newline at end of file diff --git a/1-js/3-writing-js/index.md b/1-js/3-writing-js/index.md deleted file mode 100644 index 96320c44..00000000 --- a/1-js/3-writing-js/index.md +++ /dev/null @@ -1,8 +0,0 @@ -# Качество кода - -Для того, чтобы код был качественным, необходимы как минимум: -
        -
      1. Умение отладить код и поправить ошибки.
      2. -
      3. Хороший стиль кода.
      4. -
      5. Тестировать код, желательно -- в автоматическом режиме.
      6. -
      \ No newline at end of file diff --git a/1-js/4-data-structures/1-properties-and-methods/article.md b/1-js/4-data-structures/1-properties-and-methods/article.md deleted file mode 100644 index 2deef52d..00000000 --- a/1-js/4-data-structures/1-properties-and-methods/article.md +++ /dev/null @@ -1,118 +0,0 @@ -# Введение в методы и свойства - -Все значения в JavaScript, за исключением `null` и `undefined`, содержат набор вспомогательных функций и значений, доступных "через точку". - -Такие функции называют "методами", а значения -- "свойствами". Здесь мы рассмотрим основы использования свойств и методов. - -[cut] - -## Свойство str.length - -У строки есть *свойство* `length`, содержащее длину: - -```js -//+ run -alert( "Привет, мир!".length ); // 12 -``` - -Можно и записать строку в переменную, а потом запросить её свойство: - -```js -//+ run -var str = "Привет, мир!"; -alert( str.length ); // 12 -``` - -## Метод str.toUpperCase() - -Также у строк есть *метод* `toUpperCase()`, который возвращает строку в верхнем регистре: - -```js -//+ run -var hello = "Привет, мир!"; - -*!* -alert( hello.toUpperCase() ); // "ПРИВЕТ, МИР!" -*/!* -``` - -[warn header="Вызов метода -- через круглые скобки!"] - -Обратите внимание, для вызова метода обязательно нужны круглые скобки. - -Посмотрите, например, результат обращения к `toUpperCase` без скобок: - -```js -//+ run -var hello = "Привет"; - -*!* -alert( hello.toUpperCase ); // function... -*/!* -``` - -Метод -- это встроенная команда ("функция", мы поговорим о них позже), которую нужно вызвать для получения значения. При обращении без скобок мы получим саму эту функцию. Как правило браузер выведет её как-то так: `"function toUpperCase() { ... }"`. - -А чтобы получить результат -- нужно произвести её вызов, добавив скобки: - -```js -//+ run -var hello = "Привет"; - -*!* -alert( hello.toUpperCase() ); // ПРИВЕТ -*/!* -``` - -[/warn] - -Более подробно с различными свойствами и методами строк мы познакомимся в главе [](/string). - -## Метод num.toFixed(n) - -Есть методы и у чисел, например `num.toFixed(n)`. Он округляет число `num` до `n` знаков после запятой, при необходимости добивает нулями до данной длины и возвращает в виде строки (удобно для форматированного вывода): - -```js -//+ run -var n = 12.345; - -alert( n.toFixed(2) ); // "12.35" -alert( n.toFixed(0) ); // "12" -alert( n.toFixed(5) ); // "12.34500" -``` - -Детали работы `toFixed` разобраны в главе [](/number). - -[warn header="Обращение к методам чисел"] -К методу числа можно обратиться и напрямую: - -```js -//+ run -alert( 12.34.toFixed(1) ); // 12.3 -``` - -...Но если число целое, то будет проблема: - -```js -//+ run no-beautify -alert(12.toFixed(1)); // ошибка! -``` - -Ошибка произойдёт потому, что JavaScript ожидает десятичную дробь после точки. - -Это -- особенность синтаксиса JavaScript. Вот так -- будет работать: - -```js -//+ run -alert( 12..toFixed(1) ); // 12.0 -``` - -[/warn] - -## Итого - -В этой главе мы познакомились с методами и свойствами. - -Почти все значения в JavaScript, кроме разве что `null` и `undefined` имеют их и предоставляют через них разный функционал. - -Далее мы подробно разберём основные свойства и методы структур данных в JavaScript. diff --git a/1-js/4-data-structures/10-arguments-pseudoarray/1-check-arguments-undefined/solution.md b/1-js/4-data-structures/10-arguments-pseudoarray/1-check-arguments-undefined/solution.md deleted file mode 100644 index 492f3c7a..00000000 --- a/1-js/4-data-structures/10-arguments-pseudoarray/1-check-arguments-undefined/solution.md +++ /dev/null @@ -1,12 +0,0 @@ -Узнать количество реально переданных аргументов можно по значению `arguments.length`: - -```js -//+ run -function f(x) { - alert( arguments.length ? 1 : 0 ); -} - -f(undefined); -f(); -``` - diff --git a/1-js/4-data-structures/10-arguments-pseudoarray/1-check-arguments-undefined/task.md b/1-js/4-data-structures/10-arguments-pseudoarray/1-check-arguments-undefined/task.md deleted file mode 100644 index bc3afc77..00000000 --- a/1-js/4-data-structures/10-arguments-pseudoarray/1-check-arguments-undefined/task.md +++ /dev/null @@ -1,16 +0,0 @@ -# Проверка на аргумент-undefined - -[importance 5] - -Как в функции отличить отсутствующий аргумент от `undefined`? - -```js -function f(x) { - // ..ваш код.. - // выведите 1, если первый аргумент есть, и 0 - если нет -} - -f(undefined); // 1 -f(); // 0 -``` - diff --git a/1-js/4-data-structures/10-arguments-pseudoarray/2-sum-arguments/solution.md b/1-js/4-data-structures/10-arguments-pseudoarray/2-sum-arguments/solution.md deleted file mode 100644 index 1cc6573b..00000000 --- a/1-js/4-data-structures/10-arguments-pseudoarray/2-sum-arguments/solution.md +++ /dev/null @@ -1,21 +0,0 @@ - - -```js -//+ run -function sum() { - var result = 0; - - for (var i = 0; i < arguments.length; i++) { - result += arguments[i]; - } - - return result; -} - -alert( sum() ); // 0 -alert( sum(1) ); // 1 -alert( sum(1, 2) ); // 3 -alert( sum(1, 2, 3) ); // 6 -alert( sum(1, 2, 3, 4) ); // 10 -``` - diff --git a/1-js/4-data-structures/10-arguments-pseudoarray/2-sum-arguments/task.md b/1-js/4-data-structures/10-arguments-pseudoarray/2-sum-arguments/task.md deleted file mode 100644 index 771e3d76..00000000 --- a/1-js/4-data-structures/10-arguments-pseudoarray/2-sum-arguments/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Сумма аргументов - -[importance 5] - -Напишите функцию `sum(...)`, которая возвращает сумму всех своих аргументов: - -```js -sum() = 0 -sum(1) = 1 -sum(1, 2) = 3 -sum(1, 2, 3) = 6 -sum(1, 2, 3, 4) = 10 -``` - diff --git a/1-js/4-data-structures/10-arguments-pseudoarray/article.md b/1-js/4-data-structures/10-arguments-pseudoarray/article.md deleted file mode 100644 index 12bf784e..00000000 --- a/1-js/4-data-structures/10-arguments-pseudoarray/article.md +++ /dev/null @@ -1,428 +0,0 @@ -# Псевдомассив аргументов "arguments" - -В JavaScript любая функция может быть вызвана с произвольным количеством аргументов. - -[cut] -Например: - -```js -//+ run no-beautify -function go(a,b) { - alert("a="+a+", b="+b); -} - -go(1); // a=1, b=undefined -go(1,2); // a=1, b=2 -go(1,2,3); // a=1, b=2, третий аргумент не вызовет ошибку -``` - -[smart header="В JavaScript нет \"перегрузки\" функций"] - -В некоторых языках программист может создать две функции с одинаковым именем, но разным набором аргументов, а при вызове интерпретатор сам выберет нужную: - -```js -function log(a) { - ... -} - -function log(a, b, c) { - ... -} - -*!* -log(a); // вызовется первая функция -log(a, b, c); // вызовется вторая функция -*/!* -``` - -Это называется "полиморфизмом функций" или "перегрузкой функций". В JavaScript ничего подобного нет. - -**Может быть только одна функция с именем `log`, которая вызывается с любыми аргументами.** - -А уже внутри она может посмотреть, с чем вызвана и по-разному отработать. - -В примере выше второе объявление `log` просто переопределит первое. -[/smart] - -## Доступ к "лишним" аргументам - -Как получить значения аргументов, которых нет в списке параметров? - -Доступ к ним осуществляется через "псевдо-массив" arguments. - -Он содержит список аргументов по номерам: `arguments[0]`, `arguments[1]`..., а также свойство `length`. - -Например, выведем список всех аргументов: - -```js -//+ run -function sayHi() { - for (var i = 0; i < arguments.length; i++) { - alert( "Привет, " + arguments[i] ); - } -} - -sayHi("Винни", "Пятачок"); // 'Привет, Винни', 'Привет, Пятачок' -``` - -Все параметры находятся в `arguments`, даже если они есть в списке. Код выше сработал бы также, будь функция объявлена `sayHi(a,b,c)`. - - -[warn header="Связь между `arguments` и параметрами"] - -**В старом стандарте JavaScript псевдо-массив `arguments` и переменные-параметры ссылаются на одни и те же значения.** - -В результате изменения `arguments` влияют на параметры и наоборот. - -Например: - -```js -//+ run -function f(x) { - arguments[0] = 5; // меняет переменную x - alert( x ); // 5 -} - -f(1); -``` - -Наоборот: - -```js -//+ run -function f(x) { - x = 5; - alert( arguments[0] ); // 5, обновленный x -} - -f(1); -``` - -В современной редакции стандарта это поведение изменено. Аргументы отделены от локальных переменных: - -```js -//+ run -function f(x) { - "use strict"; // для браузеров с поддержкой строгого режима - - arguments[0] = 5; - alert( x ); // не 5, а 1! Переменная "отвязана" от arguments -} - -f(1); -``` - -**Если вы не используете строгий режим, то чтобы переменные не менялись "неожиданно", рекомендуется никогда не изменять `arguments`.** -[/warn] - -### arguments -- это не массив - -Частая ошибка новичков -- попытка применить методы `Array` к `arguments`. Это невозможно: - -```js -//+ run -function sayHi() { - var a = arguments.shift(); // ошибка! нет такого метода! -} - -sayHi(1); -``` - -Дело в том, что `arguments` -- это не массив `Array`. - -В действительности, это обычный объект, просто ключи числовые и есть `length`. На этом сходство заканчивается. Никаких особых методов у него нет, и методы массивов он тоже не поддерживает. - -Впрочем, никто не мешает сделать обычный массив из `arguments`, например так: - -```js -//+ run -var args = []; -for (var i = 0; i < arguments.length; i++) { - args[i] = arguments[i]; -} -``` - -Такие объекты иногда называют *"коллекциями"* или *"псевдомассивами"*. - -## Пример: копирование свойств copy(dst, src1, src2...) [#copy] - -Иногда встаёт задача -- скопировать в существующий объект свойства из одного или нескольких других. - -Напишем для этого функцию `copy`. Она будет работать с любым числом аргументов, благодаря использованию `arguments`. - -Синтаксис: -
      -
      copy(dst, src1, src2...)
      -
      Копирует свойства из объектов `src1, src2,...` в объект `dst`. Возвращает получившийся объект.
      -
      - -Использование: - -
        -
      • Для объединения нескольких объектов в один: - -```js -//+ run -var vasya = { - age: 21, - name: 'Вася', - surname: 'Петров' -}; - -var user = { - isAdmin: false, - isEmailConfirmed: true -}; - -var student = { - university: 'My university' -}; - -// добавить к vasya свойства из user и student -*!* -copy(vasya, user, student); -*/!* - -alert( vasya.isAdmin ); // false -alert( vasya.university ); // My university -``` - -
      • -
      • Для создания копии объекта `user`: - -```js -// скопирует все свойства в пустой объект -var userClone = copy({}, user); -``` - -Такой "клон" объекта может пригодиться там, где мы хотим изменять его свойства, при этом не трогая исходный объект `user`. - -В нашей реализации мы будем копировать только свойства первого уровня, то есть вложенные объекты как-то особым образом не обрабатываются. Впрочем, её можно расширить.
      • -
      - -А вот и реализация: - -```js -//+ autorun -function copy() { - var dst = arguments[0]; - - for (var i = 1; i < arguments.length; i++) { - var arg = arguments[i]; - for (var key in arg) { - dst[key] = arg[key]; - } - } - - return dst; -} -``` - -Здесь первый аргумент `copy` -- это объект, в который нужно копировать, он назван `dst`. Для упрощения доступа к нему можно указать его прямо в объявлении функции: - -```js -*!* -function copy(dst) { -*/!* - // остальные аргументы остаются безымянными - for (var i = 1; i < arguments.length; i++) { - var arg = arguments[i]; - for (var key in arg) { - dst[key] = arg[key]; - } - } - - return dst; -} -``` - -### Аргументы по умолчанию через || - -Если функция вызвана с меньшим количеством аргументов, чем указано, то отсутствующие аргументы считаются равными `undefined`. - -Зачастую в случае отсутствия аргумента мы хотим присвоить ему некоторое "стандартное" значение или, иначе говоря, значение "по умолчанию". Это можно удобно сделать при помощи оператора логическое ИЛИ `||`. - -Например, функция `showWarning`, описанная ниже, должна показывать предупреждение. Для этого она принимает ширину `width`, высоту `height`, заголовок `title` и содержимое `contents`, но большая часть этих аргументов необязательна: - -```js -function showWarning(width, height, title, contents) { - width = width || 200; // если не указана width, то width = 200 - height = height || 100; // если нет height, то height = 100 - title = title || "Предупреждение"; - - //... -} -``` - -Это отлично работает в тех ситуациях, когда "нормальное" значение параметра в логическом контексте отлично от `false`. В коде выше, при передаче `width = 0` или `width = null`, оператор ИЛИ заменит его на значение по умолчанию. - -А что, если мы хотим использовать значение по умолчанию только если `width === undefined`? В этом случае оператор ИЛИ уже не подойдёт, нужно поставить явную проверку: - -```js -function showWarning(width, height, title, contents) { - if (width === undefined) width = 200; - if (height === undefined) height = 100; - if (title === undefined) title = "Предупреждение"; - - //... -} -``` - -## Устаревшее свойство arguments.callee [#arguments-callee] - -[warn header="Используйте NFE вместо `arguments.callee`"] -Это свойство устарело, при `use strict` оно не работает. - -Единственная причина, по которой оно тут -- это то, что его можно встретить в старом коде, поэтому о нём желательно знать. - -Современная спецификация рекомендует использовать ["именованные функциональные выражения (NFE)"](#functions-nfe). - -[/warn] - -В старом стандарте JavaScript объект `arguments` не только хранил список аргументов, но и содержал в свойстве `arguments.callee` ссылку на функцию, которая выполняется в данный момент. - -Например: - -```js -//+ run -function f() { - alert( arguments.callee === f ); // true -} - -f(); -``` - -Эти два примера будут работать одинаково: - -```js -// подвызов через NFE -var factorial = function f(n) { - return n==1 ? 1 : n**!*f(n-1)*/!*; -}; - -// подвызов через arguments.callee -var factorial = function(n) { - return n==1 ? 1 : n**!*arguments.callee(n-1)*/!*; -}; -``` - -В учебнике мы его использовать не будем, оно приведено для общего ознакомления. - -### arguments.callee.caller - -Устаревшее свойство `arguments.callee.caller` хранит ссылку на *функцию, которая вызвала данную*. - -[warn header="Это свойство тоже устарело"] -Это свойство было в старом стандарте, при `use strict` оно не работает, как и `arguments.callee`. - -Также ранее существовало более короткое свойство `arguments.caller`. Но это уже раритет, оно даже не кросс-браузерное. А вот свойство `arguments.callee.caller` поддерживается везде, если не использован `use strict`, поэтому в старом коде оно встречается. -[/warn] - -Пример работы: - -```js -//+ run -f1(); - -function f1() { - alert( arguments.callee.caller ); // null, меня вызвали из глобального кода - f2(); -} - -function f2() { - alert( arguments.callee.caller ); // f1, функция, из которой меня вызвали - f3(); -} - -function f3() { - alert( arguments.callee.caller ); // f2, функция, из которой меня вызвали -} -``` - -В учебнике мы это свойство также не будем использовать. - - -## "Именованные аргументы" - -*Именованные аргументы* -- альтернативная техника работы с аргументами, которая вообще не использует `arguments`. - -Некоторые языки программирования позволяют передать параметры как-то так: `f(width=100, height=200)`, то есть по именам, а что не передано, тех аргументов нет. Это очень удобно в тех случаях, когда аргументов много, сложно запомнить их порядок и большинство вообще не надо передавать, по умолчанию подойдёт. - -Такая ситуация часто встречается в компонентах интерфейса. Например, у "меню" может быть масса настроек отображения, которые можно "подкрутить" но обычно нужно передать всего один-два главных параметра, а остальные возьмутся по умолчанию. - -В JavaScript для этих целей используется передача аргументов в виде объекта, а в его свойствах мы передаём параметры. - -Получается так: - -```js -function showWarning(options) { - var width = options.width || 200; // по умолчанию - var height = options.height || 100; - - var title = options.title || "Предупреждение"; - - // ... -} - -showWarning({ -``` - -Вызвать такую функцию очень легко. Достаточно передать объект аргументов, указав в нем только нужные: - -```js -showWarning({ - contents: "Вы вызвали функцию" // и всё понятно! -}); -``` - -Сравним это с передачей аргументов через список: - -```js -showWarning(null, null, "Предупреждение!"); -// мысль программиста "а что это за null, null в начале? ох, надо глядеть описание функции" -``` - -Не правда ли, объект -- гораздо проще и понятнее? - -Еще один бонус кроме красивой записи -- возможность повторного использования объекта аргументов: - -```js -var opts = { - width: 400, - height: 200, - contents: "Текст" -}; - -showWarning(opts); - -opts.contents = "Другой текст"; - -*!* -showWarning(opts); // вызвать с новым текстом, без копирования других аргументов -*/!* -``` - -Именованные аргументы применяются во многих JavaScript-фреймворках. - - - - -## Итого - -
        -
      • Полный список аргументов, с которыми вызвана функция, доступен через `arguments`.
      • -
      • Это псевдомассив, то есть объект, который похож на массив, в нём есть нумерованные свойства и `length`, но методов массива у него нет.
      • -
      • В старом стандарте было свойство `arguments.callee` со ссылкой на текущую функцию, а также свойство `arguments.callee.caller`, содержащее ссылку на функцию, которая вызвала данную. Эти свойства устарели, при `use strict` обращение к ним приведёт к ошибке.
      • -
      • Для указания аргументов по умолчанию, в тех случаях, когда они заведомо не `false`, удобен оператор `||`.
      • -
      - -В тех случаях, когда возможных аргументов много и, в особенности, когда большинство их имеют значения по умолчанию, вместо работы с `arguments` организуют передачу данных через объект, который как правило называют `options`. - -Возможен и гибридный подход, при котором первый аргумент обязателен, а второй -- `options`, который содержит всевозможные дополнительные параметры: - -```js -function showMessage(text, options) { - // показать сообщение text, настройки показа указаны в options -} -``` - diff --git a/1-js/4-data-structures/11-datetime/1-new-date/solution.md b/1-js/4-data-structures/11-datetime/1-new-date/solution.md deleted file mode 100644 index 6ae34f4f..00000000 --- a/1-js/4-data-structures/11-datetime/1-new-date/solution.md +++ /dev/null @@ -1,10 +0,0 @@ -Дата в местной временной зоне создается при помощи `new Date`. - -Месяцы начинаются с нуля, так что февраль имеет номер 1. Параметры можно указывать с точностью до минут: - -```js -//+ run -var d = new Date(2012, 1, 20, 3, 12); -alert( d ); -``` - diff --git a/1-js/4-data-structures/11-datetime/1-new-date/task.md b/1-js/4-data-structures/11-datetime/1-new-date/task.md deleted file mode 100644 index c96882ac..00000000 --- a/1-js/4-data-structures/11-datetime/1-new-date/task.md +++ /dev/null @@ -1,7 +0,0 @@ -# Создайте дату - -[importance 5] - -Создайте объект `Date` для даты: 20 февраля 2012 года, 3 часа 12 минут. - -Временная зона -- местная. Выведите его на экран. \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/2-get-week-day/_js.view/solution.js b/1-js/4-data-structures/11-datetime/2-get-week-day/_js.view/solution.js deleted file mode 100644 index b007feda..00000000 --- a/1-js/4-data-structures/11-datetime/2-get-week-day/_js.view/solution.js +++ /dev/null @@ -1,5 +0,0 @@ -function getWeekDay(date) { - var days = ['вс', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб']; - - return days[date.getDay()]; -} \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/2-get-week-day/_js.view/test.js b/1-js/4-data-structures/11-datetime/2-get-week-day/_js.view/test.js deleted file mode 100644 index 5b57b663..00000000 --- a/1-js/4-data-structures/11-datetime/2-get-week-day/_js.view/test.js +++ /dev/null @@ -1,29 +0,0 @@ -describe("getWeekDay", function() { - it("3 января 2014 - пятница", function() { - assert.equal(getWeekDay(new Date(2014, 0, 3)), 'пт'); - }); - - it("4 января 2014 - суббота", function() { - assert.equal(getWeekDay(new Date(2014, 0, 4)), 'сб'); - }); - - it("5 января 2014 - воскресенье", function() { - assert.equal(getWeekDay(new Date(2014, 0, 5)), 'вс'); - }); - - it("6 января 2014 - понедельник", function() { - assert.equal(getWeekDay(new Date(2014, 0, 6)), 'пн'); - }); - - it("7 января 2014 - вторник", function() { - assert.equal(getWeekDay(new Date(2014, 0, 7)), 'вт'); - }); - - it("8 января 2014 - среда", function() { - assert.equal(getWeekDay(new Date(2014, 0, 8)), 'ср'); - }); - - it("9 января 2014 - четверг", function() { - assert.equal(getWeekDay(new Date(2014, 0, 9)), 'чт'); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/2-get-week-day/solution.md b/1-js/4-data-structures/11-datetime/2-get-week-day/solution.md deleted file mode 100644 index 29c3e37a..00000000 --- a/1-js/4-data-structures/11-datetime/2-get-week-day/solution.md +++ /dev/null @@ -1,22 +0,0 @@ -Метод `getDay()` позволяет получить номер дня недели, начиная с воскресенья. - -Запишем имена дней недели в массив, чтобы можно было их достать по номеру: - -```js -//+ run -function getWeekDay(date) { - var days = ['вс', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб']; - - return days[date.getDay()]; -} - -var date = new Date(2014, 0, 3); // 3 января 2014 -alert( getWeekDay(date) ); // 'пт' -``` - -В современных браузерах можно использовать и `toLocaleString`: -```js -//+ run -var date = new Date(2014, 0, 3); // 3 января 2014 -alert( date.toLocaleString('ru', {weekday: 'short'}) ); // 'Пт' -``` \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/2-get-week-day/task.md b/1-js/4-data-structures/11-datetime/2-get-week-day/task.md deleted file mode 100644 index 1918fdbc..00000000 --- a/1-js/4-data-structures/11-datetime/2-get-week-day/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Имя дня недели - -[importance 5] - -Создайте функцию `getWeekDay(date)`, которая выводит текущий день недели в коротком формате 'пн', 'вт', ... 'вс'. - -Например: - -```js -//+ no-beautify -var date = new Date(2012,0,3); // 3 января 2012 -alert( getWeekDay(date) ); // Должно вывести 'вт' -``` - diff --git a/1-js/4-data-structures/11-datetime/3-weekday/_js.view/solution.js b/1-js/4-data-structures/11-datetime/3-weekday/_js.view/solution.js deleted file mode 100644 index 429ce1e9..00000000 --- a/1-js/4-data-structures/11-datetime/3-weekday/_js.view/solution.js +++ /dev/null @@ -1,10 +0,0 @@ -function getLocalDay(date) { - - var day = date.getDay(); - - if (day == 0) { // день 0 становится 7 - day = 7; - } - - return day; -} \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/3-weekday/_js.view/test.js b/1-js/4-data-structures/11-datetime/3-weekday/_js.view/test.js deleted file mode 100644 index 940be025..00000000 --- a/1-js/4-data-structures/11-datetime/3-weekday/_js.view/test.js +++ /dev/null @@ -1,29 +0,0 @@ -describe("getLocalDay возвращает день недели", function() { - it("3 января 2014 - пятница", function() { - assert.equal(getLocalDay(new Date(2014, 0, 3)), 5); - }); - - it("4 января 2014 - суббота", function() { - assert.equal(getLocalDay(new Date(2014, 0, 4)), 6); - }); - - it("5 января 2014 - воскресенье", function() { - assert.equal(getLocalDay(new Date(2014, 0, 5)), 7); - }); - - it("6 января 2014 - понедельник", function() { - assert.equal(getLocalDay(new Date(2014, 0, 6)), 1); - }); - - it("7 января 2014 - вторник", function() { - assert.equal(getLocalDay(new Date(2014, 0, 7)), 2); - }); - - it("8 января 2014 - среда", function() { - assert.equal(getLocalDay(new Date(2014, 0, 8)), 3); - }); - - it("9 января 2014 - четверг", function() { - assert.equal(getLocalDay(new Date(2014, 0, 9)), 4); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/3-weekday/solution.md b/1-js/4-data-structures/11-datetime/3-weekday/solution.md deleted file mode 100644 index 80508b2b..00000000 --- a/1-js/4-data-structures/11-datetime/3-weekday/solution.md +++ /dev/null @@ -1,19 +0,0 @@ -Решение - в использовании встроенной функции `getDay`. Она полностью подходит нашим целям, но для воскресенья возвращает 0 вместо 7: - -```js -//+ run -function getLocalDay(date) { - - var day = date.getDay(); - - if (day == 0) { // день 0 становится 7 - day = 7; - } - - return day; -} - -alert( getLocalDay(new Date(2012, 0, 3)) ); // 2 -``` - -Если удобнее, чтобы день недели начинался с нуля, то можно возвращать в функции `day - 1`, тогда дни будут от 0 (пн) до 6(вс). \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/3-weekday/task.md b/1-js/4-data-structures/11-datetime/3-weekday/task.md deleted file mode 100644 index 9f2d264e..00000000 --- a/1-js/4-data-structures/11-datetime/3-weekday/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# День недели в европейской нумерации - -[importance 5] - -Напишите функцию, `getLocalDay(date)` которая возвращает день недели для даты `date`. - -День нужно возвратить в европейской нумерации, т.е. понедельник имеет номер 1, вторник номер 2, ..., воскресенье - номер 7. - -```js -//+ no-beautify -var date = new Date(2012, 0, 3); // 3 янв 2012 -alert( getLocalDay(date) ); // вторник, выведет 2 -``` - diff --git a/1-js/4-data-structures/11-datetime/4-get-date-ago/_js.view/solution.js b/1-js/4-data-structures/11-datetime/4-get-date-ago/_js.view/solution.js deleted file mode 100644 index f63438c6..00000000 --- a/1-js/4-data-structures/11-datetime/4-get-date-ago/_js.view/solution.js +++ /dev/null @@ -1,6 +0,0 @@ -function getDateAgo(date, days) { - var dateCopy = new Date(date); - - dateCopy.setDate(date.getDate() - days); - return dateCopy.getDate(); -} \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/4-get-date-ago/_js.view/test.js b/1-js/4-data-structures/11-datetime/4-get-date-ago/_js.view/test.js deleted file mode 100644 index b8d25bf5..00000000 --- a/1-js/4-data-structures/11-datetime/4-get-date-ago/_js.view/test.js +++ /dev/null @@ -1,27 +0,0 @@ -describe("getDateAgo", function() { - - it("1 день до 02.01.2015 -> число 1", function() { - assert.equal(getDateAgo(new Date(2015, 0, 2), 1), 1); - }); - - - it("2 день до 02.01.2015 -> число 31", function() { - assert.equal(getDateAgo(new Date(2015, 0, 2), 2), 31); - }); - - it("100 дней от 02.01.2015 -> число 24", function() { - assert.equal(getDateAgo(new Date(2015, 0, 2), 100), 24); - }); - - it("365 дней от 02.01.2015 -> число 2", function() { - assert.equal(getDateAgo(new Date(2015, 0, 2), 365), 2); - }); - - it("не меняет переданный объект Date", function() { - var date = new Date(2015, 0, 2); - var dateCopy = new Date(date); - getDateAgo(dateCopy, 100); - assert.equal(date.getTime(), dateCopy.getTime()); - }); - -}); \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/4-get-date-ago/solution.md b/1-js/4-data-structures/11-datetime/4-get-date-ago/solution.md deleted file mode 100644 index aa1bcb0b..00000000 --- a/1-js/4-data-structures/11-datetime/4-get-date-ago/solution.md +++ /dev/null @@ -1,29 +0,0 @@ -Из даты `date` нужно вычесть указанное количество дней. Это просто: - -```js -function getDateAgo(date, days) { - date.setDate(date.getDate() - days); - return date.getDate(); -} -``` - -Ситуацию осложняет то, что исходный объект даты не должен меняться. Это разумное требование, оно позволит избежать сюрпризов. - -Для того чтобы ему соответствовать, создадим копию объекта даты: - -```js -//+ run -function getDateAgo(date, days) { - var dateCopy = new Date(date); - - dateCopy.setDate(date.getDate() - days); - return dateCopy.getDate(); -} - -var date = new Date(2015, 0, 2); - -alert( getDateAgo(date, 1) ); // 1, (1 января 2015) -alert( getDateAgo(date, 2) ); // 31, (31 декабря 2014) -alert( getDateAgo(date, 365) ); // 2, (2 января 2014) -``` - diff --git a/1-js/4-data-structures/11-datetime/4-get-date-ago/task.md b/1-js/4-data-structures/11-datetime/4-get-date-ago/task.md deleted file mode 100644 index 0db7847b..00000000 --- a/1-js/4-data-structures/11-datetime/4-get-date-ago/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# День указанное количество дней назад - -[importance 4] - -Создайте функцию `getDateAgo(date, days)`, которая возвращает число, которое было `days` дней назад от даты `date`. - -Например, для 2 января 2015: - -```js -var date = new Date(2015, 0, 2); - -alert( getDateAgo(date, 1) ); // 1, (1 января 2015) -alert( getDateAgo(date, 2) ); // 31, (31 декабря 2014) -alert( getDateAgo(date, 365) ); // 2, (2 января 2014) -``` - -P.S. Важная деталь: в процессе вычислений функция не должна менять переданный ей объект `date`. diff --git a/1-js/4-data-structures/11-datetime/5-last-day-of-month/_js.view/solution.js b/1-js/4-data-structures/11-datetime/5-last-day-of-month/_js.view/solution.js deleted file mode 100644 index 4463d1d7..00000000 --- a/1-js/4-data-structures/11-datetime/5-last-day-of-month/_js.view/solution.js +++ /dev/null @@ -1,4 +0,0 @@ -function getLastDayOfMonth(year, month) { - var date = new Date(year, month + 1, 0); - return date.getDate(); -} \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/5-last-day-of-month/_js.view/test.js b/1-js/4-data-structures/11-datetime/5-last-day-of-month/_js.view/test.js deleted file mode 100644 index f345d490..00000000 --- a/1-js/4-data-structures/11-datetime/5-last-day-of-month/_js.view/test.js +++ /dev/null @@ -1,13 +0,0 @@ -describe("getLastDayOfMonth", function() { - it("последний день 01.01.2012 - 31", function() { - assert.equal(getLastDayOfMonth(2012, 0), 31); - }); - - it("последний день 01.02.2012 - 29 (високосный год)", function() { - assert.equal(getLastDayOfMonth(2012, 1), 29); - }); - - it("последний день 01.02.2013 - 28", function() { - assert.equal(getLastDayOfMonth(2013, 1), 28); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/5-last-day-of-month/solution.md b/1-js/4-data-structures/11-datetime/5-last-day-of-month/solution.md deleted file mode 100644 index 3bbca8c5..00000000 --- a/1-js/4-data-structures/11-datetime/5-last-day-of-month/solution.md +++ /dev/null @@ -1,14 +0,0 @@ -Создадим дату из следующего месяца, но день не первый, а "нулевой" (т.е. предыдущий): - -```js -//+ run -function getLastDayOfMonth(year, month) { - var date = new Date(year, month + 1, 0); - return date.getDate(); -} - -alert( getLastDayOfMonth(2012, 0) ); // 31 -alert( getLastDayOfMonth(2012, 1) ); // 29 -alert( getLastDayOfMonth(2013, 1) ); // 28 -``` - diff --git a/1-js/4-data-structures/11-datetime/5-last-day-of-month/task.md b/1-js/4-data-structures/11-datetime/5-last-day-of-month/task.md deleted file mode 100644 index 8f88e971..00000000 --- a/1-js/4-data-structures/11-datetime/5-last-day-of-month/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Последний день месяца? - -[importance 5] - -Напишите функцию `getLastDayOfMonth(year, month)`, которая возвращает последний день месяца. - -Параметры: -
        -
      • `year` -- 4-значный год, например 2012.
      • -
      • `month` -- месяц от 0 до 11.
      • -
      - -Например, `getLastDayOfMonth(2012, 1) = 29` (високосный год, февраль). diff --git a/1-js/4-data-structures/11-datetime/6-get-seconds-today/solution.md b/1-js/4-data-structures/11-datetime/6-get-seconds-today/solution.md deleted file mode 100644 index 1ecd8219..00000000 --- a/1-js/4-data-structures/11-datetime/6-get-seconds-today/solution.md +++ /dev/null @@ -1,28 +0,0 @@ -Для вывода достаточно сгенерировать объект `Date`, соответствующий началу дня, т.е. "сегодня" 00 часов 00 минут 00 секунд, и вычесть его из текущей даты. - -Полученная разница -- это как раз количество миллисекунд от начала дня, которое достаточно поделить на `1000`, чтобы получить секунды: - -```js -//+ run -function getSecondsToday() { - var now = new Date(); - - // создать объект из текущей даты, без часов-минут-секунд - var today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - - var diff = now - today; // разница в миллисекундах - return Math.round(diff / 1000); // перевести в секунды -} - -alert( getSecondsToday() ); -``` - -Альтернативное решение -- получить часы/минуты/секунды и преобразовать их все в секунды: - -```js -//+ run -function getSecondsToday() { - var d = new Date(); - return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); -}; -``` \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/6-get-seconds-today/task.md b/1-js/4-data-structures/11-datetime/6-get-seconds-today/task.md deleted file mode 100644 index 0b8cde4a..00000000 --- a/1-js/4-data-structures/11-datetime/6-get-seconds-today/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Сколько секунд уже прошло сегодня? - -[importance 5] - -Напишите функцию `getSecondsToday()` которая возвращает, сколько секунд прошло с начала сегодняшнего дня. - -Например, если сейчас `10:00` и не было перехода на зимнее/летнее время, то: - -```js -getSecondsToday() == 36000 // (3600 * 10) -``` - -Функция должна работать в любой день, т.е. в ней не должно быть конкретного значения сегодняшней даты. \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/7-get-seconds-to-tomorrow/solution.md b/1-js/4-data-structures/11-datetime/7-get-seconds-to-tomorrow/solution.md deleted file mode 100644 index 8aade9c9..00000000 --- a/1-js/4-data-structures/11-datetime/7-get-seconds-to-tomorrow/solution.md +++ /dev/null @@ -1,16 +0,0 @@ -Для получения оставшихся до конца дня миллисекунд нужно из "завтра 00ч 00мин 00сек" вычесть текущее время. - -Чтобы сгенерировать "завтра" -- увеличим текущую дату на 1 день: - -```js -//+ run -function getSecondsToTomorrow() { - var now = new Date(); - - // создать объект из завтрашней даты, без часов-минут-секунд - var tomorrow = new Date(now.getFullYear(), now.getMonth(), *!*now.getDate()+1*/!*); - - var diff = tomorrow - now; // разница в миллисекундах - return Math.round(diff / 1000); // перевести в секунды -} -``` diff --git a/1-js/4-data-structures/11-datetime/7-get-seconds-to-tomorrow/task.md b/1-js/4-data-structures/11-datetime/7-get-seconds-to-tomorrow/task.md deleted file mode 100644 index 93980a4f..00000000 --- a/1-js/4-data-structures/11-datetime/7-get-seconds-to-tomorrow/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Сколько секунд - до завтра? - -[importance 5] - -Напишите функцию `getSecondsToTomorrow()` которая возвращает, сколько секунд осталось до завтра. - -Например, если сейчас `23:00`, то: - -```js -getSecondsToTomorrow() == 3600 -``` - -P.S. Функция должна работать в любой день, т.е. в ней не должно быть конкретного значения сегодняшней даты. \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/8-format-date-ddmmyy/_js.view/solution.js b/1-js/4-data-structures/11-datetime/8-format-date-ddmmyy/_js.view/solution.js deleted file mode 100644 index e9498c8f..00000000 --- a/1-js/4-data-structures/11-datetime/8-format-date-ddmmyy/_js.view/solution.js +++ /dev/null @@ -1,13 +0,0 @@ -function formatDate(date) { - - var dd = date.getDate(); - if (dd < 10) dd = '0' + dd; - - var mm = date.getMonth() + 1; - if (mm < 10) mm = '0' + mm; - - var yy = date.getFullYear() % 100; - if (yy < 10) yy = '0' + yy; - - return dd + '.' + mm + '.' + yy; -} \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/8-format-date-ddmmyy/_js.view/test.js b/1-js/4-data-structures/11-datetime/8-format-date-ddmmyy/_js.view/test.js deleted file mode 100644 index 79c28e36..00000000 --- a/1-js/4-data-structures/11-datetime/8-format-date-ddmmyy/_js.view/test.js +++ /dev/null @@ -1,13 +0,0 @@ -describe("formatDate", function() { - it("правильно форматирует дату 30.01.14", function() { - assert.equal(formatDate(new Date(2014, 0, 30)), '30.01.14'); - }); - - it("правильно форматирует дату 01.01.01", function() { - assert.equal(formatDate(new Date(2001, 0, 1)), '01.01.01'); - }); - - it("правильно форматирует дату 01.01.00", function() { - assert.equal(formatDate(new Date(2000, 0, 1)), '01.01.00'); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/8-format-date-ddmmyy/solution.md b/1-js/4-data-structures/11-datetime/8-format-date-ddmmyy/solution.md deleted file mode 100644 index 7f752c89..00000000 --- a/1-js/4-data-structures/11-datetime/8-format-date-ddmmyy/solution.md +++ /dev/null @@ -1,51 +0,0 @@ -Получим компоненты один за другим. -
        -
      1. День можно получить как `date.getDate()`. При необходимости добавим ведущий ноль: - -```js -var dd = date.getDate(); -if (dd < 10) dd = '0' + dd; -``` - -
      2. -
      3. `date.getMonth()` возвратит месяц, начиная с нуля. Увеличим его на 1: - -```js -var mm = date.getMonth() + 1; // месяц 1-12 -if (mm < 10) mm = '0' + mm; -``` - -
      4. -
      5. `date.getFullYear()` вернет год в 4-значном формате. Чтобы сделать его двузначным - воспользуемся оператором взятия остатка `'%'`: - -```js -var yy = date.getFullYear() % 100; -if (yy < 10) yy = '0' + yy; -``` - -Заметим, что год, как и другие компоненты, может понадобиться дополнить нулем слева, причем возможно что `yy == 0` (например, 2000 год). При сложении со строкой `0+'0' == '00'`, так что будет все в порядке. -
      6. -
      - -Полный код: - -```js -//+ run -function formatDate(date) { - - var dd = date.getDate(); - if (dd < 10) dd = '0' + dd; - - var mm = date.getMonth() + 1; - if (mm < 10) mm = '0' + mm; - - var yy = date.getFullYear() % 100; - if (yy < 10) yy = '0' + yy; - - return dd + '.' + mm + '.' + yy; -} - -var d = new Date(2014, 0, 30); // 30 Янв 2014 -alert( formatDate(d) ); // '30.01.14' -``` - diff --git a/1-js/4-data-structures/11-datetime/8-format-date-ddmmyy/task.md b/1-js/4-data-structures/11-datetime/8-format-date-ddmmyy/task.md deleted file mode 100644 index 820b0a4b..00000000 --- a/1-js/4-data-structures/11-datetime/8-format-date-ddmmyy/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Вывести дату в формате дд.мм.гг - -[importance 3] - -Напишите функцию `formatDate(date)`, которая выводит дату `date` в формате `дд.мм.гг`: - -Например: - -```js -var d = new Date(2014, 0, 30); // 30 января 2014 -alert( formatDate(d) ); // '30.01.14' -``` - -P.S. Обратите внимание, ведущие нули должны присутствовать, то есть 1 января 2001 должно быть 01.01.01, а не 1.1.1. \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/9-format-date-relative/_js.view/solution.js b/1-js/4-data-structures/11-datetime/9-format-date-relative/_js.view/solution.js deleted file mode 100644 index dd68b99a..00000000 --- a/1-js/4-data-structures/11-datetime/9-format-date-relative/_js.view/solution.js +++ /dev/null @@ -1,34 +0,0 @@ -function formatDate(date) { - var diff = new Date() - date; // разница в миллисекундах - - if (diff < 1000) { // прошло менее 1 секунды - return 'только что'; - } - - var sec = Math.floor(diff / 1000); // округлить diff до секунд - - if (sec < 60) { - return sec + ' сек. назад'; - } - - var min = Math.floor(diff / 60000); // округлить diff до минут - if (min < 60) { - return min + ' мин. назад'; - } - - // форматировать дату, с учетом того, что месяцы начинаются с 0 - var d = date; - d = [ - '0' + d.getDate(), - '0' + (d.getMonth() + 1), - '' + d.getFullYear(), - '0' + d.getHours(), - '0' + d.getMinutes() - ]; - - for (var i = 0; i < d.length; i++) { - d[i] = d[i].slice(-2); - } - - return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':'); -} \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/9-format-date-relative/_js.view/test.js b/1-js/4-data-structures/11-datetime/9-format-date-relative/_js.view/test.js deleted file mode 100644 index f3efc135..00000000 --- a/1-js/4-data-structures/11-datetime/9-format-date-relative/_js.view/test.js +++ /dev/null @@ -1,18 +0,0 @@ -describe("formatDate", function() { - it("выводит дату 1мс назад как \"только что\"", function() { - assert.equal(formatDate(new Date(new Date - 1)), 'только что'); - }); - - it('выводит дату "30 сек назад"', function() { - assert.equal(formatDate(new Date(new Date - 30 * 1000)), "30 сек. назад"); - }); - - it('выводит дату "5 мин назад"', function() { - assert.equal(formatDate(new Date(new Date - 5 * 60 * 1000)), "5 мин. назад"); - }); - - it("выводит старую дату в формате дд.мм.гг чч:мм", function() { - assert.equal(formatDate(new Date(2014, 2, 1, 11, 22, 33)), "01.03.14 11:22"); - }); - -}); \ No newline at end of file diff --git a/1-js/4-data-structures/11-datetime/9-format-date-relative/solution.md b/1-js/4-data-structures/11-datetime/9-format-date-relative/solution.md deleted file mode 100644 index fb842d36..00000000 --- a/1-js/4-data-structures/11-datetime/9-format-date-relative/solution.md +++ /dev/null @@ -1,48 +0,0 @@ -Для того, чтобы узнать время от `date` до текущего момента - используем вычитание дат. - -```js -//+ run -function formatDate(date) { - var diff = new Date() - date; // разница в миллисекундах - - if (diff < 1000) { // прошло менее 1 секунды - return 'только что'; - } - - var sec = Math.floor(diff / 1000); // округлить diff до секунд - - if (sec < 60) { - return sec + ' сек. назад'; - } - - var min = Math.floor(diff / 60000); // округлить diff до минут - if (min < 60) { - return min + ' мин. назад'; - } - - // форматировать дату, с учетом того, что месяцы начинаются с 0 - var d = date; - d = [ - '0' + d.getDate(), - '0' + (d.getMonth() + 1), - '' + d.getFullYear(), - '0' + d.getHours(), - '0' + d.getMinutes() - ]; - - for (var i = 0; i < d.length; i++) { - d[i] = d[i].slice(-2); - } - - return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':'); -} - -alert( formatDate(new Date(new Date - 1)) ); // только что - -alert( formatDate(new Date(new Date - 30 * 1000)) ); // 30 сек. назад - -alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // 5 мин. назад - -alert( formatDate(new Date(new Date - 86400 * 1000)) ); // вчерашняя дата в формате "дд.мм.гг чч:мм" -``` - diff --git a/1-js/4-data-structures/11-datetime/9-format-date-relative/task.md b/1-js/4-data-structures/11-datetime/9-format-date-relative/task.md deleted file mode 100644 index 1e13df91..00000000 --- a/1-js/4-data-structures/11-datetime/9-format-date-relative/task.md +++ /dev/null @@ -1,26 +0,0 @@ -# Относительное форматирование даты - -[importance 4] - -Напишите функцию `formatDate(date)`, которая форматирует дату `date` так: -
        -
      • Если со времени `date` прошло менее секунды, то возвращает `"только что"`.
      • -
      • Иначе если со времени `date` прошло менее минуты, то `"n сек. назад"`.
      • -
      • Иначе если прошло меньше часа, то `"m мин. назад"`.
      • -
      • Иначе полная дата в формате `"дд.мм.гг чч:мм"`.
      • -
      - -Например: - -```js -function formatDate(date) { /* ваш код */ } - -alert( formatDate(new Date(new Date - 1)) ); // "только что" - -alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 сек. назад" - -alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 мин. назад" - -alert( formatDate(new Date(new Date - 86400 * 1000)) ); // вчерашняя дата в формате "дд.мм.гг чч:мм" -``` - diff --git a/1-js/4-data-structures/11-datetime/article.md b/1-js/4-data-structures/11-datetime/article.md deleted file mode 100644 index b1066817..00000000 --- a/1-js/4-data-structures/11-datetime/article.md +++ /dev/null @@ -1,519 +0,0 @@ -# Дата и Время - -Для работы с датой и временем в JavaScript используются объекты [Date](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/). - -[cut] -## Создание - -Для создания нового объекта типа `Date` используется один из синтаксисов: -
      -
      `new Date()`
      -
      Создает объект `Date` с текущей датой и временем: - -```js -//+ run -var now = new Date(); -alert( now ); -``` - -
      -
      `new Date(milliseconds)`
      -
      Создает объект `Date`, значение которого равно количеству миллисекунд (1/1000 секунды), прошедших с 1 января 1970 года GMT+0. - -```js -//+ run -// 24 часа после 01.01.1970 GMT+0 -var Jan02_1970 = new Date(3600 * 24 * 1000); -alert( Jan02_1970 ); -``` - -
      -
      `new Date(datestring)`
      -
      Если единственный аргумент - строка, используется вызов `Date.parse` (см. далее) для чтения даты из неё.
      -
      `new Date(year, month, date, hours, minutes, seconds, ms)`
      -
      Дату можно создать, используя компоненты в местной временной зоне. Для этого формата обязательны только первые два аргумента. Отсутствующие параметры, начиная с `hours` считаются равными нулю, а `date` -- единице. - -Заметим: -
        -
      • Год `year` должен быть из 4 цифр.
      • -
      • Отсчет месяцев `month` начинается с нуля 0.
      • -
      - -Например: - -```js -new Date(2011, 0, 1, 0, 0, 0, 0); // // 1 января 2011, 00:00:00 -new Date(2011, 0, 1); // то же самое, часы/секунды по умолчанию равны 0 -``` - -Дата задана с точностью до миллисекунд: - -```js -//+ run -var date = new Date(2011, 0, 1, 2, 3, 4, 567); -alert( date ); // 1.01.2011, 02:03:04.567 -``` - -
      -
      - - -## Получение компонентов даты - -Для доступа к компонентам даты-времени объекта `Date` используются следующие методы: -
      -
      `getFullYear()`
      -
      Получить год(из 4 цифр)
      -
      `getMonth()`
      -
      Получить месяц, **от 0 до 11**.
      -
      `getDate()`
      -
      Получить число месяца, от 1 до 31.
      -
      `getHours(), getMinutes(), getSeconds(), getMilliseconds()`
      -
      Получить соответствующие компоненты.
      -
      - -[warn header="Не `getYear()`, а `getFullYear()`"] -Некоторые браузеры реализуют нестандартный метод `getYear()`. Где-то он возвращает только две цифры из года, где-то четыре. Так или иначе, этот метод отсутствует в стандарте JavaScript. Не используйте его. Для получения года есть `getFullYear()`. -[/warn] - -Дополнительно можно получить день недели: -
      -
      `getDay()`
      -
      Получить номер дня в неделе. Неделя в JavaScript начинается с воскресенья, так что результат будет числом **от 0(воскресенье) до 6(суббота)**.
      -
      - -**Все методы, указанные выше, возвращают результат для местной временной зоны.** - -Существуют также UTC-варианты этих методов, возвращающие день, месяц, год и т.п. для зоны GMT+0 (UTC): `getUTCFullYear()`, `getUTCMonth()`, `getUTCDay()`. То есть, сразу после `"get"` вставляется `"UTC"`. - -Если ваше локальное время сдвинуто относительно UTC, то следующий код покажет разные часы: - -```js -//+ run -// текущая дата -var date = new Date(); - -// час в текущей временной зоне -alert( date.getHours() ); - -// сколько сейчас времени в Лондоне? -// час в зоне GMT+0 -alert( date.getUTCHours() ); -``` - -Кроме описанных выше, существуют два специальных метода без UTC-варианта: - -
      -
      `getTime()`
      -
      Возвращает число миллисекунд, прошедших с 1 января 1970 года GMT+0, то есть того же вида, который используется в конструкторе `new Date(milliseconds)`.
      -
      `getTimezoneOffset()`
      -
      Возвращает разницу между местным и UTC-временем, в минутах. - -```js -//+ run -alert( new Date().getTimezoneOffset() ); // Для GMT-1 выведет 60 -``` - -
      -
      - - - -## Установка компонентов даты - -Следующие методы позволяют устанавливать компоненты даты и времени: -
        -
      • `setFullYear(year [, month, date])`
      • -
      • `setMonth(month [, date])`
      • -
      • `setDate(date)`
      • -
      • `setHours(hour [, min, sec, ms])`
      • -
      • `setMinutes(min [, sec, ms])`
      • -
      • `setSeconds(sec [, ms])`
      • -
      • `setMilliseconds(ms)`
      • -
      • `setTime(milliseconds)` (устанавливает всю дату по миллисекундам с 01.01.1970 UTC)
      • -
      - -Все они, кроме `setTime()`, обладают также UTC-вариантом, например: `setUTCHours()`. - -Как видно, некоторые методы могут устанавливать несколько компонентов даты одновременно, в частности, `setHours`. При этом если какая-то компонента не указана, она не меняется. Например: - -```js -//+ run -var today = new Date; - -today.setHours(0); -alert( today ); // сегодня, но час изменён на 0 - -today.setHours(0, 0, 0, 0); -alert( today ); // сегодня, ровно 00:00:00. -``` - -### Автоисправление даты - -*Автоисправление* -- очень удобное свойство объектов `Date`. Оно заключается в том, что можно устанавливать заведомо некорректные компоненты (например 32 января), а объект сам себя поправит. - -```js -//+ run -var d = new Date(2013, 0, *!*32*/!*); // 32 января 2013 ?!? -alert(d); // ... это 1 февраля 2013! -``` - -**Неправильные компоненты даты автоматически распределяются по остальным.** - -Например, нужно увеличить на 2 дня дату "28 февраля 2011". Может быть так, что это будет 2 марта, а может быть и 1 марта, если год високосный. Но нам обо всем этом думать не нужно. Просто прибавляем два дня. Остальное сделает `Date`: - -```js -//+ run -var d = new Date(2011, 1, 28); -*!* -d.setDate(d.getDate() + 2); -*/!* - -alert( d ); // 2 марта, 2011 -``` - -Также это используют для получения даты, отдаленной от имеющейся на нужный промежуток времени. Например, получим дату на 70 секунд большую текущей: - -```js -//+ run -var d = new Date(); -d.setSeconds(d.getSeconds() + 70); - -alert( d ); // выведет корректную дату -``` - -Можно установить и нулевые, и даже отрицательные компоненты. Например: - -```js -//+ run -var d = new Date; - -d.setDate(1); // поставить первое число месяца -alert( d ); - -d.setDate(0); // нулевого числа нет, будет последнее число предыдущего месяца -alert( d ); -``` - - - -```js -//+ run -var d = new Date; - -d.setDate(-1); // предпоследнее число предыдущего месяца -alert( d ); -``` - -### Преобразование к числу, разность дат - -Когда объект `Date` используется в числовом контексте, он преобразуется в количество миллисекунд: - -```js -//+ run -alert(+new Date) // +date то же самое, что: +date.valueOf() -``` - -**Важный побочный эффект: даты можно вычитать, результат вычитания объектов `Date` -- их временная разница, в миллисекундах**. - -Это используют для измерения времени: - -```js -//+ run -var start = new Date; // засекли время - -// что-то сделать -for (var i = 0; i < 100000; i++) { - var doSomething = i * i * i; -} - -var end = new Date; // конец измерения - -alert( "Цикл занял " + (end - start) + " ms" ); -``` - -### Бенчмаркинг - -Допустим, у нас есть несколько вариантов решения задачи, каждый описан функцией. - -Как узнать, какой быстрее? - -Для примера возьмем две функции, которые бегают по массиву: - -```js -function walkIn(arr) { - for (var key in arr) arr[i]++ -} - -function walkLength(arr) { - for (var i = 0; i < arr.length; i++) arr[i]++; -} -``` - -Чтобы померять, какая из них быстрее, нельзя запустить один раз `walkIn`, один раз `walkLength` и замерить разницу. Одноразовый запуск ненадежен, любая мини-помеха исказит результат. - -Для правильного бенчмаркинга функция запускается много раз, чтобы сам тест занял существенное время. Это сведет влияние помех к минимуму. Сложную функцию можно запускать 100 раз, простую -- 1000 раз... - -Померяем, какая из функций быстрее: - -```js -//+ run -var arr = []; -for (var i = 0; i < 1000; i++) arr[i] = 0; - -function walkIn(arr) { - for (var key in arr) arr[i]++; -} - -function walkLength(arr) { - for (var i = 0; i < arr.length; i++) arr[i]++; -} - -function bench(f) { - var date = new Date(); - for (var i = 0; i < 10000; i++) f(arr); - return new Date() - date; -} - -alert( 'Время walkIn: ' + bench(walkIn) + 'мс' ); -alert( 'Время walkLength: ' + bench(walkLength) + 'мс' ); -``` - -Теперь представим себе, что во время первого бенчмаркинга `bench(walkIn)` компьютер что-то делал параллельно важное (вдруг) и это занимало ресурсы, а во время второго -- перестал. Реальная ситуация? Конечно реальна, особенно на современных ОС, где много процессов одновременно. - -**Гораздо более надёжные результаты можно получить, если весь пакет тестов прогнать много раз.** - -```js -//+ run -var arr = []; -for (var i = 0; i < 1000; i++) arr[i] = 0; - -function walkIn(arr) { - for (var key in arr) arr[i]++; -} - -function walkLength(arr) { - for (var i = 0; i < arr.length; i++) arr[i]++; -} - -function bench(f) { - var date = new Date(); - for (var i = 0; i < 1000; i++) f(arr); - return new Date() - date; -} - -*!* -// bench для каждого теста запустим много раз, чередуя -var timeIn = 0, - timeLength = 0; -for (var i = 0; i < 100; i++) { - timeIn += bench(walkIn); - timeLength += bench(walkLength); -} -*/!* - -alert( 'Время walkIn: ' + timeIn + 'мс' ); -alert( 'Время walkLength: ' + timeLength + 'мс' ); -``` - -[smart header="Более точное время с `performance.now()`"] -В современных браузерах (кроме IE9-) вызов [performance.now()](https://developer.mozilla.org/en-US/docs/Web/API/performance.now) возвращает количество миллисекунд, прошедшее с начала загрузки страницы. Причём именно с самого начала, до того, как загрузился HTML-файл, если точнее -- с момента выгрузки предыдущей страницы из памяти. - -Так что это время включает в себя всё, включая начальное обращение к серверу. - -Его можно посмотреть в любом месте страницы, даже в ``, чтобы узнать, сколько времени потребовалось браузеру, чтобы до него добраться, включая загрузку HTML. - -Возвращаемое значение измеряется в миллисекундах, но дополнительно имеет точность 3 знака после запятой (до миллионных долей секунды!), поэтому можно использовать его и для более точного бенчмаркинга в том числе. -[/smart] - -[smart header="`console.time(метка)` и `console.timeEnd(метка)`"] -Для измерения с одновременным выводом результатов в консоли есть методы: -
        -
      • `console.time(метка)` -- включить внутренний хронометр браузера с меткой.
      • -
      • `console.timeEnd(метка)` -- выключить внутренний хронометр браузера с меткой и вывести результат.
      • -
      -Параметр `"метка"` используется для идентификации таймера, чтобы можно было делать много замеров одновременно и даже вкладывать измерения друг в друга. - -В коде ниже таймеры `walkIn`, `walkLength` -- конкретные тесты, а таймер "All Benchmarks" -- время "на всё про всё": - -```js -//+ run -var arr = []; -for (var i = 0; i < 1000; i++) arr[i] = 0; - -function walkIn(arr) { - for (var key in arr) arr[i]++; -} - -function walkLength(arr) { - for (var i = 0; i < arr.length; i++) arr[i]++; -} - -function bench(f) { - for (var i = 0; i < 10000; i++) f(arr); -} - -console.time("All Benchmarks"); - -console.time("walkIn"); -bench(walkIn); -console.timeEnd("walkIn"); - - -console.time("walkLength"); -bench(walkLength); -console.timeEnd("walkLength"); - -console.timeEnd("All Benchmarks"); -``` - -**При запуске этого примера нужно открыть консоль, иначе вы ничего не увидите.** -[/smart] - -[warn header="Внимание, оптимизатор!"] -Современные интерпретаторы JavaScript делают массу оптимизаций, например: -
        -
      1. Автоматически выносят инвариант, то есть постоянное в цикле значение типа `arr.length`, за пределы цикла.
      2. -
      3. Стараются понять, значения какого типа хранит данная переменная или массив, какую структуру имеет объект и, исходя из этого, оптимизировать внутренние алгоритмы.
      4. -
      5. Выполняют простейшие операции, например сложение явно заданных чисел и строк, на этапе компиляции.
      6. -
      7. Могут обнаружить, что некий код, например присваивание к неиспользуемой локальной переменной, ни на что не влияет и вообще исключить его из выполнения, хотя делают это редко.
      8. -
      -Эти оптимизации могут влиять на результаты тестов, поэтому измерять скорость базовых операций JavaScript ("проводить микробенчмаркинг") до того, как вы изучите внутренности JavaScript-интерпретаторов и поймёте, что они реально делают на таком коде, не рекомендуется. -[/warn] - - -## Форматирование и вывод дат - -Во всех браузерах, кроме IE10-, поддерживается новый стандарт [Ecma 402](http://www.ecma-international.org/publications/standards/Ecma-402.htm), который добавляет специальные методы для форматирования дат. - -Это делается вызовом `date.toLocaleString(локаль, опции)`, в котором можно задать много настроек. Он позволяет указать, какие параметры даты нужно вывести, и ряд настроек вывода, после чего интерпретатор сам сформирует строку. - -Пример с почти всеми параметрами даты и русским, затем английским (США) форматированием: - -```js -//+ run -var date = new Date(2014, 11, 31, 12, 30, 0); - -var options = { - era: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - weekday: 'long', - timezone: 'UTC', - hour: 'numeric', - minute: 'numeric', - second: 'numeric' -}; - -alert( date.toLocaleString("ru", options) ); // среда, 31 декабря 2014 г. н.э. 12:30:00 -alert( date.toLocaleString("en-US", options) ); // Wednesday, December 31, 2014 Anno Domini 12:30:00 PM -``` - -Вы сможете подробно узнать о них в статье [](/intl), которая посвящена этому стандарту. - - -**Методы вывода без локализации:** - -
      -
      `toString()`, `toDateString()`, `toTimeString()`
      -
      Возвращают стандартное строчное представление, не заданное жёстко в стандарте, а зависящее от браузера. Единственное требование к нему -- читаемость человеком. Метод `toString` возвращает дату целиком, `toDateString()` и `toTimeString()` -- только дату и время соответственно. - -```js -//+ run -var d = new Date(); - -alert( d.toString() ); // вывод, похожий на 'Wed Jan 26 2011 16:40:50 GMT+0300' -``` - -
      `toUTCString()`
      -
      То же самое, что `toString()`, но дата в зоне UTC.
      -
      -
      `toISOString()`
      -
      Возвращает дату в формате ISO Детали формата будут далее. Поддерживается современными браузерами, не поддерживается IE8-. - -```js -//+ run -var d = new Date(); - -alert( d.toISOString() ); // вывод, похожий на '2011-01-26T13:51:50.417Z' -``` - -
      - -Если хочется иметь большую гибкость и кросс-браузерность, то также можно воспользоваться специальной библиотекой, например [Moment.JS](http://momentjs.com/) или написать свою функцию форматирования. - - - -## Разбор строки, Date.parse - -Все современные браузеры, включая IE9+, понимают даты в упрощённом формате ISO 8601 Extended. - -Этот формат выглядит так: `YYYY-MM-DDTHH:mm:ss.sssZ`, где: - -
        -
      • `YYYY-MM-DD` -- дата в формате год-месяц-день.
      • -
      • Обычный символ `T` используется как разделитель.
      • -
      • `HH:mm:ss.sss` -- время: часы-минуты-секунды-миллисекунды.
      • -
      • Часть `'Z'` обозначает временную зону -- в формате `+-hh:mm`, либо символ `Z`, обозначающий UTC. По стандарту её можно не указывать, тогда UTC, но в Safari с этим ошибка, так что лучше указывать всегда.
      • -
      - -Также возможны укороченные варианты, например `YYYY-MM-DD` или `YYYY-MM` или даже только `YYYY`. - -Метод `Date.parse(str)` разбирает строку `str` в таком формате и возвращает соответствующее ей количество миллисекунд. Если это невозможно, `Date.parse` возвращает `NaN`. - -Например: - -```js -//+ run -var msUTC = Date.parse('2012-01-26T13:51:50.417Z'); // зона UTC - -alert( msUTC ); // 1327571510417 (число миллисекунд) -``` - -С таймзоной `-07:00 GMT`: - -```js -//+ run -var ms = Date.parse('2012-01-26T13:51:50.417-07:00'); - -alert( ms ); // 1327611110417 (число миллисекунд) -``` - - -[smart header="Формат дат для IE8-"] -До появления спецификации EcmaScript 5 формат не был стандартизован, и браузеры, включая IE8-, имели свои собственные форматы дат. Частично, эти форматы пересекаются. - -Например, код ниже работает везде, включая старые IE: - -```js -//+ run -var ms = Date.parse("January 26, 2011 13:51:50"); - -alert( ms ); -``` - -Вы также можете почитать о старых форматах IE в документации к методу MSDN Date.parse. - -Конечно же, сейчас лучше использовать современный формат. Если же нужна поддержка IE8-, то метод `Date.parse`, как и ряд других современных методов, добавляется библиотекой [es5-shim](https://github.com/kriskowal/es5-shim). -[/smart] - -## Метод Date.now() - -Метод `Date.now()` возвращает дату сразу в виде миллисекунд. - -Технически, он аналогичен вызову `+new Date()`, но в отличие от него не создаёт промежуточный объект даты, а поэтому -- во много раз быстрее. - -Его использование особенно рекомендуется там, где производительность при работе с датами критична. Обычно это не на веб-страницах, а, к примеру, в разработке игр на JavaScript. - -## Итого - -
        -
      • Дата и время представлены в JavaScript одним объектом: [Date](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/). Создать "только время" при этом нельзя, оно должно быть с датой. Список методов `Date` вы можете найти в справочнике [Date](http://javascript.ru/Date) или выше.
      • -
      • Отсчёт месяцев начинается с нуля.
      • -
      • Отсчёт дней недели (для `getDay()`) тоже начинается с нуля (и это воскресенье).
      • -
      • Объект `Date` удобен тем, что автокорректируется. Благодаря этому легко сдвигать даты.
      • -
      • При преобразовании к числу объект `Date` даёт количество миллисекунд, прошедших с 1 января 1970 UTC. Побочное следствие -- даты можно вычитать, результатом будет разница в миллисекундах.
      • -
      • Для получения текущей даты в миллисекундах лучше использовать `Date.now()`, чтобы не создавать лишний объект `Date` (кроме IE8-)
      • -
      • Для бенчмаркинга лучше использовать `performance.now()` (кроме IE9-), он в 1000 раз точнее.
      • -
      \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/1-sum-interface/solution.md b/1-js/4-data-structures/2-number/1-sum-interface/solution.md deleted file mode 100644 index d42523ce..00000000 --- a/1-js/4-data-structures/2-number/1-sum-interface/solution.md +++ /dev/null @@ -1,11 +0,0 @@ - - -```js -//+ run demo -var a = +prompt("Введите первое число", ""); -var b = +prompt("Введите второе число", ""); - -alert( a + b ); -``` - -Обратите внимание на оператор `+` перед `prompt`, он сразу приводит вводимое значение к числу. Если бы его не было, то `a` и `b` были бы строками и складывались бы как строки, то есть `"1" + "2" = "12"`. \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/1-sum-interface/task.md b/1-js/4-data-structures/2-number/1-sum-interface/task.md deleted file mode 100644 index 2aac44a6..00000000 --- a/1-js/4-data-structures/2-number/1-sum-interface/task.md +++ /dev/null @@ -1,9 +0,0 @@ -# Интерфейс суммы - -[importance 5] - -Создайте страницу, которая предлагает ввести два числа и выводит их сумму. - -[demo /] - -P.S. Есть "подводный камень" при работе с типами. diff --git a/1-js/4-data-structures/2-number/2-why-rounded-down/solution.md b/1-js/4-data-structures/2-number/2-why-rounded-down/solution.md deleted file mode 100644 index 574bf957..00000000 --- a/1-js/4-data-structures/2-number/2-why-rounded-down/solution.md +++ /dev/null @@ -1,8 +0,0 @@ -Во внутреннем двоичном представлении `6.35` является бесконечной двоичной дробью. Хранится она с потерей точности.. А впрочем, посмотрим сами: - -```js -//+ run -alert( 6.35.toFixed(20) ); // 6.34999999999999964473 -``` - -Интерпретатор видит число как `6.34...`, поэтому и округляет вниз. \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/2-why-rounded-down/task.md b/1-js/4-data-structures/2-number/2-why-rounded-down/task.md deleted file mode 100644 index 203280c6..00000000 --- a/1-js/4-data-structures/2-number/2-why-rounded-down/task.md +++ /dev/null @@ -1,19 +0,0 @@ -# Почему 6.35.toFixed(1) == 6.3? - -[importance 4] - -В математике принято, что `5` округляется вверх, например: - -```js -//+ run -alert( 1.5.toFixed(0) ); // 2 -alert( 1.35.toFixed(1) ); // 1.4 -``` - -Но почему в примере ниже `6.35` округляется до `6.3`? - -```js -//+ run -alert( 6.35.toFixed(1) ); // 6.3 -``` - diff --git a/1-js/4-data-structures/2-number/3-sum-prices/solution.md b/1-js/4-data-structures/2-number/3-sum-prices/solution.md deleted file mode 100644 index 242dcdde..00000000 --- a/1-js/4-data-structures/2-number/3-sum-prices/solution.md +++ /dev/null @@ -1,13 +0,0 @@ -Есть два основных подхода. -
        -
      1. Можно хранить сами цены в "копейках" (центах и т.п.). Тогда они всегда будут целые и проблема исчезнет. Но при показе и при обмене данными нужно будет это учитывать и не забывать делить на 100.
      2. -
      3. При операциях, когда необходимо получить окончательный результат -- округлять до 2го знака после запятой. Все, что дальше -- ошибка округления: - -```js -//+ run no-beautify -var price1 = 0.1, price2 = 0.2; -alert( +(price1 + price2).toFixed(2) ); -``` - -
      4. -
      \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/3-sum-prices/task.md b/1-js/4-data-structures/2-number/3-sum-prices/task.md deleted file mode 100644 index 02118152..00000000 --- a/1-js/4-data-structures/2-number/3-sum-prices/task.md +++ /dev/null @@ -1,16 +0,0 @@ -# Сложение цен - -[importance 5] - -Представьте себе электронный магазин. Цены даны с точностью до копейки(цента, евроцента и т.п.). - -Вы пишете интерфейс для него. Основная работа происходит на сервере, но и на клиенте все должно быть хорошо. Сложение цен на купленные товары и умножение их на количество является обычной операцией. - -Получится глупо, если при заказе двух товаров с ценами `0.10$` и `0.20$` человек получит общую стоимость `0.30000000000000004$`: - -```js -//+ run -alert( 0.1 + 0.2 + '$' ); -``` - -Что можно сделать, чтобы избежать проблем с ошибками округления? \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/4-endless-loop-error/solution.md b/1-js/4-data-structures/2-number/4-endless-loop-error/solution.md deleted file mode 100644 index 86855efd..00000000 --- a/1-js/4-data-structures/2-number/4-endless-loop-error/solution.md +++ /dev/null @@ -1,14 +0,0 @@ -Потому что `i` никогда не станет равным `10`. - -Запустите, чтобы увидеть *реальные* значения `i`: - -```js -//+ run -var i = 0; -while (i < 11) { - i += 0.2; - if (i > 9.8 && i < 10.2) alert( i ); -} -``` - -Ни одно из них в точности не равно `10`. \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/4-endless-loop-error/task.md b/1-js/4-data-structures/2-number/4-endless-loop-error/task.md deleted file mode 100644 index e3ef1874..00000000 --- a/1-js/4-data-structures/2-number/4-endless-loop-error/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Бесконечный цикл по ошибке - -[importance 4] - -Этот цикл - бесконечный. Почему? - -```js -var i = 0; -while (i != 10) { - i += 0.2; -} -``` - diff --git a/1-js/4-data-structures/2-number/5-get-decimal/_js.view/solution.js b/1-js/4-data-structures/2-number/5-get-decimal/_js.view/solution.js deleted file mode 100644 index 7ff8569c..00000000 --- a/1-js/4-data-structures/2-number/5-get-decimal/_js.view/solution.js +++ /dev/null @@ -1,7 +0,0 @@ -function getDecimal(num) { - var str = "" + num; - var zeroPos = str.indexOf("."); - if (zeroPos == -1) return 0; - str = str.slice(zeroPos); - return +str; -} \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/5-get-decimal/_js.view/test.js b/1-js/4-data-structures/2-number/5-get-decimal/_js.view/test.js deleted file mode 100644 index 8f79eec9..00000000 --- a/1-js/4-data-structures/2-number/5-get-decimal/_js.view/test.js +++ /dev/null @@ -1,21 +0,0 @@ -describe("getDecimal", function() { - it("возвращает дробную часть 1.2 как 0.2", function() { - assert.equal(getDecimal(1.2), 0.2); - }); - - it("возвращает дробную часть 1.3 как 0.3", function() { - assert.equal(getDecimal(1.3), 0.3); - }); - - it("возвращает дробную часть 12.345 как 0.345", function() { - assert.equal(getDecimal(12.345), 0.345); - }); - - it("возвращает дробную часть -1.2 как 0.2", function() { - assert.equal(getDecimal(-1.2), 0.2); - }); - - it("возвращает дробную часть 5 как 0", function() { - assert.equal(getDecimal(5), 0); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/5-get-decimal/solution.md b/1-js/4-data-structures/2-number/5-get-decimal/solution.md deleted file mode 100644 index 9bd45ee5..00000000 --- a/1-js/4-data-structures/2-number/5-get-decimal/solution.md +++ /dev/null @@ -1,82 +0,0 @@ -# Функция - -Первая идея может быть такой: - -```js -//+ run -function getDecimal(num) { - return num - Math.floor(num); -} - -alert( getDecimal(12.5) ); // 0.5 -*!* -alert( getDecimal(-1.2) ); // 0.8, неверно! -*/!* -``` - -Как видно из примера выше, для отрицательных чисел она не работает. - -Это потому, что округление `Math.floor` происходит всегда к ближайшему меньшему целому, то есть `Math.floor(-1.2) = -2`, а нам бы хотелось убрать целую часть, т.е. получить `-1`. - -Можно попытаться решить проблему так: - -```js -//+ run -function getDecimal(num) { - return num > 0 ? num - Math.floor(num) : Math.ceil(num) - num; -} - -alert( getDecimal(12.5) ); // 0.5 -*!* -alert( getDecimal(-1.2) ); // 0.19999999999999996, неверно! -alert( getDecimal(1.2) ); // 0.19999999999999996 -*/!* -``` - -Проблема с отрицательными числами решена, но результат, увы, не совсем тот. - -Внутреннее неточное представление чисел приводит к ошибке в вычислениях, которая проявляется при работе и с положительными и с отрицательными числами. - -Давайте попробуем ещё вариант -- получим остаток при делении на `1`. При таком делении от любого числа в остатке окажется именно дробная часть: - -```js -//+ run -function getDecimal(num) { - return num > 0 ? (num % 1) : (-num % 1); -} - -alert( getDecimal(12.5) ); // 0.5 -*!* -alert( getDecimal(1.2) ); // 0.19999999999999996, неверно! -*/!* -``` - -В общем-то, работает, функция стала короче, но, увы, ошибка сохранилась. - -Что делать? - -Увы, операции с десятичными дробями подразумевают некоторую потерю точности. - -Зависит от ситуации. -
        -
      • Если внешний вид числа неважен и ошибка в вычислениях допустима -- она ведь очень мала, то можно оставить как есть.
      • -
      • Перейти на промежуточные целочисленные вычисления там, где это возможно.
      • -
      • Если мы знаем, что десятичная часть жёстко ограничена, к примеру, может содержать не более 2 знаков то можно округлить число, то есть вернуть `+num.toFixed(2)`.
      • -
      - -Если эти варианты не подходят, то можно работать с числом как со строкой: - -```js -//+ run -function getDecimal(num) { - var str = "" + num; - var zeroPos = str.indexOf("."); - if (zeroPos == -1) return 0; - str = str.slice(zeroPos); - return +str; -} - -alert( getDecimal(12.5) ); // 0.5 -alert( getDecimal(1.2) ); // 0.2 -``` - diff --git a/1-js/4-data-structures/2-number/5-get-decimal/task.md b/1-js/4-data-structures/2-number/5-get-decimal/task.md deleted file mode 100644 index ec77059b..00000000 --- a/1-js/4-data-structures/2-number/5-get-decimal/task.md +++ /dev/null @@ -1,12 +0,0 @@ -# Как получить дробную часть числа? - -[importance 4] - -Напишите функцию `getDecimal(num)`, которая возвращает десятичную часть числа: - -```js -alert( getDecimal(12.345) ); // 0.345 -alert( getDecimal(1.2) ); // 0.2 -alert( getDecimal(-1.2) ); // 0.2 -``` - diff --git a/1-js/4-data-structures/2-number/6-formula-binet/solution.md b/1-js/4-data-structures/2-number/6-formula-binet/solution.md deleted file mode 100644 index 4d3abf0b..00000000 --- a/1-js/4-data-structures/2-number/6-formula-binet/solution.md +++ /dev/null @@ -1,35 +0,0 @@ - - -```js -//+ run -function fibBinet(n) { - var phi = (1 + Math.sqrt(5)) / 2; - // используем Math.round для округления до ближайшего целого - return Math.round(Math.pow(phi, n) / Math.sqrt(5)); -} - -function fib(n) { - var a = 1, - b = 0, - x; - for (i = 0; i < n; i++) { - x = a + b; - a = b - b = x; - } - return b; -} - -alert( fibBinet(2) ); // 1, равно fib(2) -alert( fibBinet(8) ); // 21, равно fib(8) -*!* -alert( fibBinet(77) ); // 5527939700884755 -alert( fib(77) ); // 5527939700884757, не совпадает! -*/!* -``` - -**Результат вычисления F77 получился неверным!** - -Причина -- в ошибках округления, ведь √5 -- бесконечная дробь. - -Ошибки округления при вычислениях множатся и, в итоге, дают расхождение. \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/6-formula-binet/task.md b/1-js/4-data-structures/2-number/6-formula-binet/task.md deleted file mode 100644 index 706bdf19..00000000 --- a/1-js/4-data-structures/2-number/6-formula-binet/task.md +++ /dev/null @@ -1,29 +0,0 @@ -# Формула Бине - -[importance 4] - -Последовательность [чисел Фибоначчи](http://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D0%BB%D0%B0_%D0%A4%D0%B8%D0%B1%D0%BE%D0%BD%D0%B0%D1%87%D1%87%D0%B8) имеет формулу Fn = Fn-1 + Fn-2. То есть, следующее число получается как сумма двух предыдущих. - -Первые два числа равны `1`, затем `2(1+1)`, затем `3(1+2)`, `5(2+3)` и так далее: `1, 1, 2, 3, 5, 8, 13, 21...`. - -Код для их вычисления (из задачи [](/task/fibonacci-numbers)): - -```js -function fib(n) { - var a = 1, - b = 0, - x; - for (i = 0; i < n; i++) { - x = a + b; - a = b - b = x; - } - return b; -} -``` - -Существует [формула Бине](http://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D0%BB%D0%B0_%D0%A4%D0%B8%D0%B1%D0%BE%D0%BD%D0%B0%D1%87%D1%87%D0%B8#.D0.A4.D0.BE.D1.80.D0.BC.D1.83.D0.BB.D0.B0_.D0.91.D0.B8.D0.BD.D0.B5), согласно которой Fn равно ближайшему целому для ϕn/√5, где ϕ=(1+√5)/2 -- золотое сечение. - -Напишите функцию `fibBinet(n)`, которая будет вычислять Fn, используя эту формулу. Проверьте её для значения F77 (должно получиться `fibBinet(77) = 5527939700884757`). - -**Одинаковы ли результаты, полученные при помощи кода `fib(n)` выше и по формуле Бине? Если нет, то почему и какой из них верный?** \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/7-random-0-max/solution.md b/1-js/4-data-structures/2-number/7-random-0-max/solution.md deleted file mode 100644 index 985dd191..00000000 --- a/1-js/4-data-structures/2-number/7-random-0-max/solution.md +++ /dev/null @@ -1,9 +0,0 @@ -Сгенерируем значение в диапазоне `0..1` и умножим на `max`: - -```js -//+ run -var max = 10; - -alert( Math.random() * max ); -``` - diff --git a/1-js/4-data-structures/2-number/7-random-0-max/task.md b/1-js/4-data-structures/2-number/7-random-0-max/task.md deleted file mode 100644 index a7175b55..00000000 --- a/1-js/4-data-structures/2-number/7-random-0-max/task.md +++ /dev/null @@ -1,5 +0,0 @@ -# Случайное из интервала (0, max) - -[importance 2] - -Напишите код для генерации случайного значения в диапазоне от `0` до `max`, не включая `max`. diff --git a/1-js/4-data-structures/2-number/8-random-min-max/solution.md b/1-js/4-data-structures/2-number/8-random-min-max/solution.md deleted file mode 100644 index 5954a2e0..00000000 --- a/1-js/4-data-structures/2-number/8-random-min-max/solution.md +++ /dev/null @@ -1,10 +0,0 @@ -Сгенерируем значение из интервала `0..max-min`, а затем сдвинем на `min`: - -```js -//+ run -var min = 5, - max = 10; - -alert( min + Math.random() * (max - min) ); -``` - diff --git a/1-js/4-data-structures/2-number/8-random-min-max/task.md b/1-js/4-data-structures/2-number/8-random-min-max/task.md deleted file mode 100644 index e5e8bf18..00000000 --- a/1-js/4-data-structures/2-number/8-random-min-max/task.md +++ /dev/null @@ -1,5 +0,0 @@ -# Случайное из интервала (min, max) - -[importance 2] - -Напишите код для генерации случайного числа от `min` до `max`, не включая `max`. diff --git a/1-js/4-data-structures/2-number/9-random-int-min-max/solution.md b/1-js/4-data-structures/2-number/9-random-int-min-max/solution.md deleted file mode 100644 index 749c9175..00000000 --- a/1-js/4-data-structures/2-number/9-random-int-min-max/solution.md +++ /dev/null @@ -1,80 +0,0 @@ -# Очевидное неверное решение (round) - -Самый простой, но неверный способ -- это сгенерировать значение в интервале `min..max` и округлить его `Math.round`, вот так: - -```js -//+ run -function randomInteger(min, max) { - var rand = min + Math.random() * (max - min) - rand = Math.round(rand); - return rand; -} - -alert( randomInteger(1, 3) ); -``` - -Эта функция работает. Но при этом она некорректна: вероятность получить крайние значения `min` и `max` будет в два раза меньше, чем любые другие. - -При многократном запуске этого кода вы легко заметите, что `2` выпадает чаще всех. - -Это происходит из-за того, что `Math.round()` получает разнообразные случайные числа из интервала от `1` до `3`, но при округлении до ближайшего целого получится, что: - -```js -//+ no-beautify -значения из диапазона 1 ... 1.49999.. станут 1 -значения из диапазона 1.5 ... 2.49999.. станут 2 -значения из диапазона 2.5 ... 2.99999.. станут 3 -``` - -Отсюда явно видно, что в `1` (как и `3`) попадает диапазон значений в два раза меньший, чем в `2`. Из-за этого такой перекос. - -# Верное решение с round - -Правильный способ: `Math.round(случайное от min-0.5 до max+0.5)` - -```js -//+ run -*!* -function randomInteger(min, max) { - var rand = min - 0.5 + Math.random() * (max - min + 1) - rand = Math.round(rand); - return rand; - } -*/!* - -alert( randomInteger(5, 10) ); -``` - -В этом случае диапазон будет тот же (`max-min+1`), но учтена механика округления `round`. - -# Решение с floor - -Альтернативный путь - применить округление `Math.floor()` к случайному числу от `min` до `max+1`. - -Например, для генерации целого числа от `1` до `3`, создадим вспомогательное случайное значение от `1` до `4` (не включая `4`). - -Тогда `Math.floor()` округлит их так: - -```js -//+ no-beautify -1 ... 1.999+ станет 1 -2 ... 2.999+ станет 2 -3 ... 3.999+ станет 3 -``` - -Все диапазоны одинаковы. -Итак, код: - -```js -//+ run -*!* -function randomInteger(min, max) { - var rand = min + Math.random() * (max + 1 - min); - rand = Math.floor(rand); - return rand; - } -*/!* - -alert( randomInteger(5, 10) ); -``` - diff --git a/1-js/4-data-structures/2-number/9-random-int-min-max/task.md b/1-js/4-data-structures/2-number/9-random-int-min-max/task.md deleted file mode 100644 index 7f747a25..00000000 --- a/1-js/4-data-structures/2-number/9-random-int-min-max/task.md +++ /dev/null @@ -1,7 +0,0 @@ -# Случайное целое от min до max - -[importance 2] - -Напишите функцию `randomInteger(min, max)` для генерации случайного **целого** числа между `min` и `max`, включая `min,max` как возможные значения. - -Любое число из интервала `min..max` должно иметь одинаковую вероятность. diff --git a/1-js/4-data-structures/2-number/article.md b/1-js/4-data-structures/2-number/article.md deleted file mode 100644 index b25a936b..00000000 --- a/1-js/4-data-structures/2-number/article.md +++ /dev/null @@ -1,612 +0,0 @@ -# Числа - -Все числа в JavaScript, как целые так и дробные, имеют тип `Number` и хранятся в 64-битном формате [IEEE-754](http://en.wikipedia.org/wiki/IEEE_754-1985), также известном как "double precision". - -Здесь мы рассмотрим различные тонкости, связанные с работой с числами в JavaScript. - -## Способы записи - -В JavaScript можно записывать числа не только в десятичной, но и в шестнадцатеричной (начинается с `0x`), а также восьмеричной (начинается с `0`) системах счисления: - -```js -//+ run -alert( 0xFF ); // 255 в шестнадцатиричной системе -alert( 010 ); // 8 в восьмеричной системе -``` - -Также доступна запись в *"научном формате"* (ещё говорят "запись с плавающей точкой"), который выглядит как `<число>e<кол-во нулей>`. - -Например, `1e3` -- это `1` с `3` нулями, то есть `1000`. - -```js -//+ run -// еще пример научной формы: 3 с 5 нулями -alert( 3e5 ); // 300000 -``` - -Если количество нулей отрицательно, то число сдвигается вправо за десятичную точку, так что получается десятичная дробь: - -```js -//+ run -// здесь 3 сдвинуто 5 раз вправо, за десятичную точку. -alert( 3e-5 ); // 0.00003 <-- 5 нулей, включая начальный ноль -``` - -## Деление на ноль, Infinity - -Представьте, что вы собираетесь создать новый язык... Люди будут называть его "JavaScript" (или LiveScript... неважно). - -Что должно происходить при попытке деления на ноль? - -Как правило, ошибка в программе... Во всяком случае, в большинстве языков программирования это именно так. - -Но создатель JavaScript решил пойти математически правильным путем. Ведь чем меньше делитель, тем больше результат. При делении на очень-очень маленькое число должно получиться очень большое. В математическом анализе это описывается через [пределы](http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D0%B4%D0%B5%D0%BB_(%D0%BC%D0%B0%D1%82%D0%B5%D0%BC%D0%B0%D1%82%D0%B8%D0%BA%D0%B0)), и если подразумевать предел, то в качестве результата деления на `0` мы получаем "бесконечность", которая обозначается символом `∞` или, в JavaScript: `"Infinity"`. - -```js -//+ run -alert( 1 / 0 ); // Infinity -alert( 12345 / 0 ); // Infinity -``` - -**`Infinity` -- особенное численное значение, которое ведет себя в точности как математическая бесконечность `∞`.** -
        -
      • `Infinity` больше любого числа.
      • -
      • Добавление к бесконечности не меняет её.
      • -
      - -```js -//+ run -alert( Infinity > 1234567890 ); // true -alert( Infinity + 5 == Infinity ); // true -``` - -**Бесконечность можно присвоить и в явном виде: `var x = Infinity`.** - -Бывает и минус бесконечность `-Infinity`: - -```js -//+ run -alert( -1 / 0 ); // -Infinity -``` - -Бесконечность можно получить также, если сделать ну очень большое число, для которого количество разрядов в двоичном представлении не помещается в соответствующую часть стандартного 64-битного формата, например: - -```js -//+ run -alert( 1e500 ); // Infinity -``` - -## NaN - -Если математическая операция не может быть совершена, то возвращается специальное значение `NaN` (Not-A-Number). - -Например, деление `0/0` в математическом смысле неопределено, поэтому его результат `NaN`: - -```js -//+ run -alert( 0 / 0 ); // NaN -``` - -Значение `NaN` используется для обозначения математической ошибки и обладает следующими свойствами: - -
        -
      • Значение `NaN` -- единственное, в своем роде, которое *не равно ничему, включая себя*. - -Следующий код ничего не выведет: - -```js -//+ run -if (NaN == NaN) alert( "==" ); // Ни один вызов -if (NaN === NaN) alert( "===" ); // не сработает -``` - -
      • -
      • Значение `NaN` можно проверить специальной функцией `isNaN(n)`, которая преобразует аргумент к числу и возвращает `true`, если получилось `NaN`, и `false` -- для любого другого значения. - -```js -//+ run -var n = 0 / 0; - -alert( isNaN(n) ); // true -alert( isNaN("12") ); // false, строка преобразовалась к обычному числу 12 -``` - -[smart header="Забавный способ проверки на `NaN`"] -Отсюда вытекает забавный способ проверки значения на `NaN`: можно проверить значение на равенство самому себе, если не равно -- то `NaN`: - -```js -//+ run -var n = 0 / 0; - -if (n !== n) alert( 'n = NaN!' ); -``` - -Это работает, но для наглядности лучше использовать `isNaN(n)`. -[/smart] - -
      • -
      • Значение `NaN` "прилипчиво". Любая операция с `NaN` возвращает `NaN`. - -```js -//+ run -alert( NaN + 1 ); // NaN -``` - -
      • -
      - -Если аргумент `isNaN` -- не число, то он автоматически преобразуется к числу. - - -[smart header="Математические операции в JS безопасны"] -Никакие математические операции в JavaScript не могут привести к ошибке или "обрушить" программу. - -В худшем случае, результат будет `NaN`. -[/smart] - -## isFinite(n) - -Итак, в JavaScript есть обычные числа и три специальных числовых значения: `NaN`, `Infinity` и `-Infinity`. - -Тот факт, что они, хоть и особые, но -- числа, демонстрируется работой оператора `+`: - -```js -//+ run -var value = prompt("Введите Infinity", 'Infinity'); - -*!* -var number = +value; -*/!* - -alert( number ); // Infinity, плюс преобразовал строку "Infinity" к такому "числу" -``` - -Обычно если мы хотим от посетителя получить число, то `Infinity` или `NaN` нам не подходят. Для того, чтобы отличить "обычные" числа от таких специальных значений, существует функция `isFinite`. - -**Функция `isFinite(n)` преобразует аргумент к числу и возвращает `true`, если это не `NaN/Infinity/-Infinity`:** - -```js -//+ run -alert( isFinite(1) ); // true -alert( isFinite(Infinity) ); // false -alert( isFinite(NaN) ); // false -``` - -## Преобразование к числу - -Большинство арифметических операций и математических функций преобразуют значение в число автоматически. - -Для того, чтобы сделать это явно, обычно перед значением ставят унарный плюс `'+'`: - -```js -//+ run -var s = "12.34"; -alert( +s ); // 12.34 -``` - -При этом, если строка не является в точности числом, то результат будет `NaN`: - -```js -//+ run -alert( +"12test" ); // NaN -``` - -Единственное исключение -- пробельные символы в начале и в конце строки, которые игнорируются: - -```js -//+ run -alert( +" -12" ); // -12 -alert( +" \n34 \n" ); // 34, перевод строки \n является пробельным символом -alert( +"" ); // 0, пустая строка становится нулем -alert( +"1 2" ); // NaN, пробел посередине числа - ошибка -``` - -Аналогичным образом происходит преобразование и в других математических операторах и функциях: - -```js -//+ run -alert( '12.34' / "-2" ); // -6.17 -``` - -## Мягкое преобразование: parseInt и parseFloat - -В мире HTML/CSS многие значения не являются в точности числами. Например, метрики CSS: `10pt` или `-12px`. - -Оператор `'+'` для таких значений возвратит `NaN`: - -```js -//+ run -alert(+"12px") // NaN -``` - -Для удобного чтения таких значений существует функция `parseInt`: - -```js -//+ run -alert( parseInt('12px') ); // 12 -``` - -**Функция `parseInt` и ее аналог `parseFloat` преобразуют строку символ за символом, пока это возможно.** - -При возникновении ошибки возвращается число, которое получилось. Функция `parseInt` читает из строки целое число, а `parseFloat` -- дробное. - -```js -//+ run -alert(parseInt('12px')) // 12, ошибка на символе 'p' -alert(parseFloat('12.3.4')) // 12.3, ошибка на второй точке -``` - -Конечно, существуют ситуации, когда `parseInt/parseFloat` возвращают `NaN`. Это происходит при ошибке на первом же символе: - -```js -//+ run -alert( parseInt('a123') ); // NaN -``` - - -## Проверка на число - -Для проверки строки на число можно использовать функцию `isNaN(str)`. - -Она преобразует строку в число аналогично `+`, а затем вернёт `true`, если это `NaN`, т.е. если преобразование не удалось: - -```js -//+ run -var x = prompt("Введите значение", "-11.5"); - -if (isNaN(x)) { - alert( "Строка преобразовалась в NaN. Не число" ); -} else { - alert( "Число" ); -} -``` - -Однако, у такой проверки есть две особенности: - -
      1. Пустая строка и строка из пробельных символов преобразуются к `0`, поэтому считаются числами.
      2. -
      3. Если применить такую проверку не к строке, то могут быть сюрпризы, в частности `isNaN` посчитает числами значения `false, true, null`, так как они хотя и не числа, но преобразуются к ним.
      4. -
      - -```js -//+ run -alert( isNaN(null) ); // false - не NaN, т.е. "число" -alert( isNaN("\n \n") ); // false - не NaN, т.е. "число" -``` - -Если такое поведение допустимо, то `isNaN` -- приемлемый вариант. - -Если же нужна действительно точная проверка на число, которая не считает числом строку из пробелов, логические и специальные значения, а также отсекает `Infinity` -- используйте следующую функцию `isNumeric`: - -```js -function isNumeric(n) { - return !isNaN(parseFloat(n)) && isFinite(n); -} -``` - -Разберёмся, как она работает. Начнём справа. - -
        -
      • Функция `isFinite(n)` преобразует аргумент к числу и возвращает `true`, если это не `Infinity/-Infinity/NaN`. - -Таким образом, правая часть отсеет заведомо не-числа, но оставит такие значения как `true/false/null` и пустую строку `''`, т.к. они корректно преобразуются в числа. -
      • -
      • Для их проверки нужна левая часть. Вызов `parseFloat(true/false/null/'')` вернёт `NaN` для этих значений. - -Так устроена функция `parseFloat`: она преобразует аргумент к строке, т.е. `true/false/null` становятся `"true"/"false"/"null"`, а затем считывает из неё число, при этом пустая строка даёт `NaN`.
      • -
      - -В результате отсеивается всё, кроме строк-чисел и обычных чисел. - -## toString(система счисления) - -Как показано выше, числа можно записывать не только в 10-чной, но и в 16-ричной системе. Но бывает и противоположная задача: получить 16-ричное представление числа. Для этого используется метод `toString(основание системы)`, например: - -```js -//+ run -var n = 255; - -alert( n.toString(16) ); // ff -``` - -В частности, это используют для работы с цветовыми значениями в браузере, вида `#AABBCC`. - -Основание может быть любым от `2` до `36`. - -
        -
      • Основание `2` бывает полезно для отладки побитовых операций: - -```js -//+ run -var n = 4; -alert( n.toString(2) ); // 100 -``` - -
      • -
      • Основание `36` (по количеству букв в английском алфавите -- 26, вместе с цифрами, которых 10) используется для того, чтобы "кодировать" число в виде буквенно-цифровой строки. В этой системе счисления сначала используются цифры, а затем буквы от `a` до `z`: - -```js -//+ run -var n = 1234567890; -alert( n.toString(36) ); // kf12oi -``` - -При помощи такого кодирования можно "укоротить" длинный цифровой идентификатор, например чтобы выдать его в качестве URL. -
      • -
      - - - - -## Округление - -Одна из самых частых операций с числом -- округление. В JavaScript существуют целых 3 функции для этого. - -
      -
      `Math.floor`
      -
      Округляет вниз
      -
      `Math.ceil`
      -
      Округляет вверх
      -
      `Math.round`
      -
      Округляет до ближайшего целого
      -
      - -```js -//+ run no-beautify -alert( Math.floor(3.1) ); // 3 -alert( Math.ceil(3.1) ); // 4 -alert( Math.round(3.1) ); // 3 -``` - -[smart header="Округление битовыми операторами"] -[Битовые операторы](/bitwise-operators) делают любое число 32-битным целым, обрезая десятичную часть. - -В результате побитовая операция, которая не изменяет число, например, двойное битовое НЕ -- округляет его: - -```js -//+ run -alert( ~~12.3 ); // 12 -``` - -Любая побитовая операция такого рода подойдет, например XOR (исключающее ИЛИ, `"^"`) с нулем: - -```js -//+ run -alert( 12.3 ^ 0 ); // 12 -alert( 1.2 + 1.3 ^ 0 ); // 2, приоритет ^ меньше, чем + -``` - -Это удобно в первую очередь тем, что легко читается и не заставляет ставить дополнительные скобки как `Math.floor(...)`: - -```js -var x = a * b / c ^ 0; // читается как "a * b / c и округлить" -``` - -[/smart] - -### Округление до заданной точности - -Для округления до нужной цифры после запятой можно умножить и поделить на 10 с нужным количеством нулей. Например, округлим `3.456` до 2го знака после запятой: - -```js -//+ run -var n = 3.456; -alert( Math.round(n * 100) / 100 ); // 3.456 -> 345.6 -> 346 -> 3.46 -``` - -Таким образом можно округлять число и вверх и вниз. - -### num.toFixed(precision) - -Существует также специальный метод `num.toFixed(precision)`, который округляет число `num` до точности `precision` и возвращает результат *в виде строки*: - -```js -//+ run -var n = 12.34; -alert( n.toFixed(1) ); // "12.3" -``` - -Округление идёт до ближайшего значения, аналогично `Math.round`: - -```js -//+ run -var n = 12.36; -alert( n.toFixed(1) ); // "12.4" -``` - -Итоговая строка, при необходимости, дополняется нулями до нужной точности: - -```js -//+ run -var n = 12.34; -alert( n.toFixed(5) ); // "12.34000", добавлены нули до 5 знаков после запятой -``` - -Если нам нужно именно число, то мы можем получить его, применив `'+'` к результату `n.toFixed(..)`: - -```js -//+ run -var n = 12.34; -alert( +n.toFixed(5) ); // 12.34 -``` - -[warn header="Метод `toFixed` не эквивалентен `Math.round`!"] -Например, произведём округление до одного знака после запятой с использованием двух способов: `toFixed` и `Math.round` с умножением и делением: - -```js -//+ run -var price = 6.35; - -alert( price.toFixed(1) ); // 6.3 -alert( Math.round(price * 10) / 10 ); // 6.4 -``` - -Как видно, результат разный! Вариант округления через `Math.round` получился более корректным, так как по общепринятым правилам `5` округляется вверх. А `toFixed` может округлить его как вверх, так и вниз. Почему? Скоро узнаем! -[/warn] - - -## Неточные вычисления - -Запустите этот пример: - -```js -//+ run -alert( 0.1 + 0.2 == 0.3 ); -``` - -Запустили? Если нет -- все же сделайте это. - -Ок, вы запустили его. Он вывел `false`. Результат несколько странный, не так ли? Возможно, ошибка в браузере? Поменяйте браузер, запустите еще раз. - -Хорошо, теперь мы можем быть уверены: `0.1 + 0.2` это не `0.3`. Но тогда что же это? - -```js -//+ run -alert( 0.1 + 0.2 ); // 0.30000000000000004 -``` - -Как видите, произошла небольшая вычислительная ошибка, результат сложения `0.1 + 0.2` немного больше, чем `0.3`. - -```js -//+ run -alert( 0.1 + 0.2 > 0.3 ); // true -``` - -Всё дело в том, что в стандарте IEEE 754 на число выделяется ровно 8 байт(=64 бита), не больше и не меньше. - -Число `0.1 (одна десятая)` записывается просто в десятичном формате, а в двоичной системе счисления это бесконечная дробь ([перевод десятичной дроби в двоичную систему](http://www.klgtu.ru/students/literature/inf_asu/1760.html)). Также бесконечной дробью является `0.2 (=2/10)`. - -Двоичное значение бесконечных дробей хранится только до определенного знака, поэтому возникает неточность. Её даже можно увидеть: - -```js -//+ run -alert( 0.1.toFixed(20) ); // 0.10000000000000000555 -``` - -Когда мы складываем `0.1` и `0.2`, то две неточности складываются, получаем незначительную, но всё же ошибку в вычислениях. - -Конечно, это не означает, что точные вычисления для таких чисел невозможны. Они возможны. И даже необходимы. - -Например, есть два способа сложить `0.1` и `0.2`: -
        -
      1. Сделать их целыми, сложить, а потом поделить: - -```js -//+ run -alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3 -``` - -Это работает, т.к. числа `0.1*10 = 1` и `0.2*10 = 2` могут быть точно представлены в двоичной системе. -
      2. -
      3. Сложить, а затем округлить до разумного знака после запятой. Округления до 10-го знака обычно бывает достаточно, чтобы отсечь ошибку вычислений: - -```js -//+ run -var result = 0.1 + 0.2; -alert( +result.toFixed(10) ); // 0.3 -``` - -
      4. -
      - - - -[smart header="Забавный пример"] -Привет! Я -- число, растущее само по себе! - -```js -//+ run -alert( 9999999999999999 ); // выведет 10000000000000000 -``` - -Причина та же -- потеря точности. - -Из `64` бит, отведённых на число, сами цифры числа занимают до `52` бит, остальные `11` бит хранят позицию десятичной точки и один бит -- знак. Так что если `52` бит не хватает на цифры, то при записи пропадут младшие разряды. - -Интерпретатор не выдаст ошибку, но в результате получится "не совсем то число", что мы и видим в примере выше. Как говорится: "как смог, так записал". - -[/smart] - -Ради справедливости заметим, что в точности то же самое происходит в любом другом языке, где используется формат IEEE 754, включая Java, C, PHP, Ruby, Perl. - -## Другие математические методы - -JavaScript предоставляет базовые тригонометрические и некоторые другие функции для работы с числами. - -### Тригонометрия - -Встроенные функции для тригонометрических вычислений: - -
      -
      `Math.acos(x)`
      -
      Возвращает арккосинус `x` (в радианах)
      -
      `Math.asin(x)`
      -
      Возвращает арксинус `x` (в радианах)
      -
      `Math.atan(x)`
      -
      Возвращает арктангенс `x` (в радианах)
      -
      `Math.atan2(y, x)`
      -
      Возвращает угол до точки `(y, x)`. Описание функции: [Atan2](http://en.wikipedia.org/wiki/Atan2).
      -
      `Math.sin(x)`
      -
      Вычисляет синус `x` (в радианах)
      -
      `Math.cos(x)`
      -
      Вычисляет косинус `x` (в радианах)
      -
      `Math.tan(x)`
      -
      Возвращает тангенс `x` (в радианах)
      -
      - -### Функции общего назначения - -Разные полезные функции: -
      -
      `Math.sqrt(x)`
      -
      Возвращает квадратный корень из `x`.
      -
      `Math.log(x)`
      -
      Возвращает натуральный (по основанию e) логарифм `x`.
      -
      `Math.pow(x, exp)`
      -
      Возводит число в степень, возвращает xexp, например `Math.pow(2,3) = 8`. Работает в том числе с дробными и отрицательными степенями, например: `Math.pow(4, -1/2) = 0.5`.
      -
      `Math.abs(x)`
      -
      Возвращает абсолютное значение числа
      -
      `Math.exp(x)`
      -
      Возвращает ex, где e -- основание натуральных логарифмов.
      -
      `Math.max(a, b, c...)`
      -
      Возвращает наибольший из списка аргументов
      -
      `Math.min(a, b, c...)`
      -
      Возвращает наименьший из списка аргументов
      -
      `Math.random()`
      -
      Возвращает псевдо-случайное число в интервале [0,1) - то есть между 0(включительно) и 1(не включая). Генератор случайных чисел инициализуется текущим временем.
      -
      - -### Форматирование - -Для красивого вывода чисел в стандарте [ECMA 402](http://www.ecma-international.org/ecma-402/1.0/ECMA-402.pdf) есть метод `toLocaleString()`: - -```js -//+ run -var number = 123456789; - -alert( number.toLocaleString() ); // 123 456 789 -``` - -Его поддерживают все современные браузеры, кроме IE10- (для которых нужно подключить библиотеку [Intl.JS](https://github.com/andyearnshaw/Intl.js/)). Он также умеет форматировать валюту и проценты. Более подробно про устройство этого метода можно будет узнать в статье [](/intl), когда это вам понадобится. - -## Итого - -
        -
      • Числа могут быть записаны в шестнадцатиричной, восьмеричной системе, а также "научным" способом.
      • -
      • В JavaScript существует числовое значение бесконечность `Infinity`.
      • -
      • Ошибка вычислений дает `NaN`.
      • -
      • Арифметические и математические функции преобразуют строку в точности в число, игнорируя начальные и конечные пробелы.
      • -
      • Функции `parseInt/parseFloat` делают числа из строк, которые начинаются с числа.
      • -
      • Есть четыре способа округления: `Math.floor`, `Math.round`, `Math.ceil` и битовый оператор. Для округления до нужного знака используйте `+n.toFixed(p)` или трюк с умножением и делением на 10p.
      • -
      • Дробные числа дают ошибку вычислений. При необходимости ее можно отсечь округлением до нужного знака.
      • -
      • Случайные числа от `0` до `1` генерируются с помощью `Math.random()`, остальные -- преобразованием из них.
      • -
      - -Существуют и другие математические функции. Вы можете ознакомиться с ними в справочнике в разделах Number и Math. - - - - - - - - diff --git a/1-js/4-data-structures/3-string/1-ucfirst/_js.view/test.js b/1-js/4-data-structures/3-string/1-ucfirst/_js.view/test.js deleted file mode 100644 index b935f642..00000000 --- a/1-js/4-data-structures/3-string/1-ucfirst/_js.view/test.js +++ /dev/null @@ -1,9 +0,0 @@ -describe("ucFirst", function() { - it('делает первый символ заглавным', function() { - assert.strictEqual(ucFirst("вася"), "Вася"); - }); - - it('для пустой строки возвращает пустую строку', function() { - assert.strictEqual(ucFirst(""), ""); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/3-string/1-ucfirst/solution.md b/1-js/4-data-structures/3-string/1-ucfirst/solution.md deleted file mode 100644 index e97aa8ec..00000000 --- a/1-js/4-data-structures/3-string/1-ucfirst/solution.md +++ /dev/null @@ -1,27 +0,0 @@ -Мы не можем просто заменить первый символ, т.к. строки в JavaScript неизменяемы. - -Но можно пересоздать строку на основе существующей, с заглавным первым символом: - -```js -var newStr = str[0].toUpperCase() + str.slice(1); -``` - -Однако, есть небольшая проблемка -- в случае, когда строка пуста, будет ошибка. - -Ведь `str[0] == undefined`, а у `undefined` нет метода `toUpperCase()`. - -Выхода два. Первый -- использовать `str.charAt(0)`, он всегда возвращает строку, для пустой строки -- пустую, но не `undefined`. Второй -- отдельно проверить на пустую строку, вот так: - -```js -//+ run -function ucFirst(str) { - // только пустая строка в логическом контексте даст false - if (!str) return str; - - return str[0].toUpperCase() + str.slice(1); -} - -alert( ucFirst("вася") ); -``` - -P.S. Возможны и более короткие решения, использующие методы для работы со строками, которые мы пройдём далее. \ No newline at end of file diff --git a/1-js/4-data-structures/3-string/1-ucfirst/task.md b/1-js/4-data-structures/3-string/1-ucfirst/task.md deleted file mode 100644 index 3c7e9965..00000000 --- a/1-js/4-data-structures/3-string/1-ucfirst/task.md +++ /dev/null @@ -1,12 +0,0 @@ -# Сделать первый символ заглавным - -[importance 5] - -Напишите функцию `ucFirst(str)`, которая возвращает строку `str` с заглавным первым символом, например: - -```js -ucFirst("вася") == "Вася"; -ucFirst("") == ""; // нет ошибок при пустой строке -``` - -P.S. В JavaScript нет встроенного метода для этого. Создайте функцию, используя `toUpperCase()` и `charAt()`. diff --git a/1-js/4-data-structures/3-string/2-check-spam/_js.view/solution.js b/1-js/4-data-structures/3-string/2-check-spam/_js.view/solution.js deleted file mode 100644 index abe6e87c..00000000 --- a/1-js/4-data-structures/3-string/2-check-spam/_js.view/solution.js +++ /dev/null @@ -1,5 +0,0 @@ -function checkSpam(str) { - var lowerStr = str.toLowerCase(); - - return !!(~lowerStr.indexOf('viagra') || ~lowerStr.indexOf('xxx')); -} \ No newline at end of file diff --git a/1-js/4-data-structures/3-string/2-check-spam/_js.view/test.js b/1-js/4-data-structures/3-string/2-check-spam/_js.view/test.js deleted file mode 100644 index 1564a6b3..00000000 --- a/1-js/4-data-structures/3-string/2-check-spam/_js.view/test.js +++ /dev/null @@ -1,13 +0,0 @@ -describe("checkSpam", function() { - it('считает спамом "buy ViAgRA now"', function() { - assert.isTrue(checkSpam('buy ViAgRA now')); - }); - - it('считает спамом "free xxxxx"', function() { - assert.isTrue(checkSpam('free xxxxx')); - }); - - it('не считает спамом "innocent rabbit"', function() { - assert.isFalse(checkSpam('innocent rabbit')); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/3-string/2-check-spam/solution.md b/1-js/4-data-structures/3-string/2-check-spam/solution.md deleted file mode 100644 index 9eecade3..00000000 --- a/1-js/4-data-structures/3-string/2-check-spam/solution.md +++ /dev/null @@ -1,17 +0,0 @@ -Метод `indexOf` ищет совпадение с учетом регистра. То есть, в строке `'xXx'` он не найдет `'XXX'`. - -Для проверки приведем к нижнему регистру и строку `str` а затем уже будем искать. - -```js -//+ run -function checkSpam(str) { - var lowerStr = str.toLowerCase(); - - return !!(~lowerStr.indexOf('viagra') || ~lowerStr.indexOf('xxx')); -} - -alert( checkSpam('buy ViAgRA now') ); -alert( checkSpam('free xxxxx') ); -alert( checkSpam("innocent rabbit") ); -``` - diff --git a/1-js/4-data-structures/3-string/2-check-spam/task.md b/1-js/4-data-structures/3-string/2-check-spam/task.md deleted file mode 100644 index 566a2d1e..00000000 --- a/1-js/4-data-structures/3-string/2-check-spam/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Проверьте спам - -[importance 5] - -Напишите функцию `checkSpam(str)`, которая возвращает `true`, если строка `str` содержит 'viagra' or 'XXX', а иначе `false`. - -Функция должна быть нечувствительна к регистру: - -```js -checkSpam('buy ViAgRA now') == true -checkSpam('free xxxxx') == true -checkSpam("innocent rabbit") == false -``` - diff --git a/1-js/4-data-structures/3-string/3-truncate/_js.view/solution.js b/1-js/4-data-structures/3-string/3-truncate/_js.view/solution.js deleted file mode 100644 index e37f1a3b..00000000 --- a/1-js/4-data-structures/3-string/3-truncate/_js.view/solution.js +++ /dev/null @@ -1,4 +0,0 @@ -function truncate(str, maxlength) { - return (str.length > maxlength) ? - str.slice(0, maxlength - 3) + '...' : str; -} \ No newline at end of file diff --git a/1-js/4-data-structures/3-string/3-truncate/_js.view/test.js b/1-js/4-data-structures/3-string/3-truncate/_js.view/test.js deleted file mode 100644 index c14666d6..00000000 --- a/1-js/4-data-structures/3-string/3-truncate/_js.view/test.js +++ /dev/null @@ -1,16 +0,0 @@ -describe("truncate", function() { - it("обрезает строку до указанной длины (включая троеточие)", function() { - assert.equal( - truncate("Вот, что мне хотелось бы сказать на эту тему:", 20), - "Вот, что мне хоте..." - ); - }); - - it("не меняет короткие строки", function() { - assert.equal( - truncate("Всем привет!", 20), - "Всем привет!" - ); - }); - -}); \ No newline at end of file diff --git a/1-js/4-data-structures/3-string/3-truncate/solution.md b/1-js/4-data-structures/3-string/3-truncate/solution.md deleted file mode 100644 index 113e68a2..00000000 --- a/1-js/4-data-structures/3-string/3-truncate/solution.md +++ /dev/null @@ -1,28 +0,0 @@ -Так как окончательная длина строки должна быть `maxlength`, то нужно её обрезать немного короче, чтобы дать место для троеточия. - -```js -//+ run -function truncate(str, maxlength) { - if (str.length > maxlength) { - return str.slice(0, maxlength - 3) + '...'; - // итоговая длина равна maxlength - } - - return str; -} - -alert( truncate("Вот, что мне хотелось бы сказать на эту тему:", 20) ); -alert( truncate("Всем привет!", 20) ); -``` - -Можно было бы написать этот код ещё короче: - -```js -//+ run -function truncate(str, maxlength) { - return (str.length > maxlength) ? - str.slice(0, maxlength - 3) + '...' : str; -} -``` - -P.S. Кстати, в кодироке Unicode существует специальный символ "троеточие": `…` (HTML: `…`), который можно использовать вместо трёх точек. Если его использовать, то можно отрезать только один символ. diff --git a/1-js/4-data-structures/3-string/3-truncate/task.md b/1-js/4-data-structures/3-string/3-truncate/task.md deleted file mode 100644 index 4904d5f8..00000000 --- a/1-js/4-data-structures/3-string/3-truncate/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# Усечение строки - -[importance 5] - -Создайте функцию `truncate(str, maxlength)`, которая проверяет длину строки `str`, и если она превосходит `maxlength` -- заменяет конец `str` на `"..."`, так чтобы ее длина стала равна `maxlength`. - -Результатом функции должна быть (при необходимости) усечённая строка. - -Например: - -```js -truncate("Вот, что мне хотелось бы сказать на эту тему:", 20) = "Вот, что мне хоте..." - -truncate("Всем привет!", 20) = "Всем привет!" -``` - -Эта функция имеет применение в жизни. Она используется, чтобы усекать слишком длинные темы сообщений. diff --git a/1-js/4-data-structures/3-string/4-extract-currency/_js.view/test.js b/1-js/4-data-structures/3-string/4-extract-currency/_js.view/test.js deleted file mode 100644 index 45e0eb8c..00000000 --- a/1-js/4-data-structures/3-string/4-extract-currency/_js.view/test.js +++ /dev/null @@ -1,8 +0,0 @@ -describe("extractCurrencyValue", function() { - - it("выделяет из строки $120 число 120", function() { - assert.strictEqual(extractCurrencyValue('$120'), 120); - }); - - -}); \ No newline at end of file diff --git a/1-js/4-data-structures/3-string/4-extract-currency/solution.md b/1-js/4-data-structures/3-string/4-extract-currency/solution.md deleted file mode 100644 index d2c8c1a0..00000000 --- a/1-js/4-data-structures/3-string/4-extract-currency/solution.md +++ /dev/null @@ -1 +0,0 @@ -Возьмём часть строки после первого символа и приведём к числу: `+str.slice(1)`. \ No newline at end of file diff --git a/1-js/4-data-structures/3-string/4-extract-currency/task.md b/1-js/4-data-structures/3-string/4-extract-currency/task.md deleted file mode 100644 index ee4691d1..00000000 --- a/1-js/4-data-structures/3-string/4-extract-currency/task.md +++ /dev/null @@ -1,8 +0,0 @@ -# Выделить число - -[importance 4] - -Есть стоимость в виде строки: `"$120"`. То есть, первым идёт знак валюты, а затем -- число. - -Создайте функцию `extractCurrencyValue(str)`, которая будет из такой строки выделять число-значение, в данном случае 120. - diff --git a/1-js/4-data-structures/3-string/article.md b/1-js/4-data-structures/3-string/article.md deleted file mode 100644 index 5205e8fb..00000000 --- a/1-js/4-data-structures/3-string/article.md +++ /dev/null @@ -1,534 +0,0 @@ -# Строки - -В JavaScript любые текстовые данные являются строками. Не существует отдельного типа "символ", который есть в ряде других языков. - -Внутренним форматом строк, вне зависимости от кодировки страницы, является [Юникод (Unicode)](http://ru.wikipedia.org/wiki/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4). -[cut] -## Создание строк - -Строки создаются при помощи двойных или одинарных кавычек: - -```js -var text = "моя строка"; - -var anotherText = 'еще строка'; - -var str = "012345"; -``` - -В JavaScript нет разницы между двойными и одинарными кавычками. - -### Специальные символы - -Строки могут содержать специальные символы. Самый часто используемый из таких символов -- это "перевод строки". - -Он обозначается как `\n`, например: - -```js -//+ run -alert( 'Привет\nМир' ); // выведет "Мир" на новой строке -``` - -Есть и более редкие символы, вот их список: - - - - - - - - - - - - - - -
      Специальные символы
      СимволОписание
      \bBackspace
      \fForm feed
      \nNew line
      \rCarriage return
      \tTab
      \uNNNNСимвол в кодировке Юникод с шестнадцатеричным кодом `NNNN`. Например, `\u00A9` -- юникодное представление символа копирайт © -
      - -### Экранирование специальных символов - -Если строка в одинарных кавычках, то внутренние одинарные кавычки внутри должны быть *экранированы*, то есть снабжены обратным слешем `\'`, вот так: - -```js -var str = '*!*I\'m*/!* a JavaScript programmer'; -``` - -В двойных кавычках -- экранируются внутренние двойные: - -```js -//+ run -var str = "I'm a JavaScript \"programmer\" "; -alert( str ); // I'm a JavaScript "programmer" -``` - -Экранирование служит исключительно для правильного восприятия строки JavaScript. В памяти строка будет содержать сам символ без `'\'`. Вы можете увидеть это, запустив пример выше. - -Сам символ обратного слэша `'\'` является служебным, поэтому всегда экранируется, т.е пишется как `\\`: - -```js -//+ run -var str = ' символ \\ '; - -alert( str ); // символ \ -``` - -Заэкранировать можно любой символ. Если он не специальный, то ничего не произойдёт: - -```js -//+ run -alert( "\a" ); // a -// идентично alert( "a" ); -``` - -## Методы и свойства - -Здесь мы рассмотрим методы и свойства строк, с некоторыми из которых мы знакомились ранее, в главе [](/properties-and-methods). - - -### Длина length - -Одно из самых частых действий со строкой -- это получение ее длины: - -```js -//+ run -var str = "My\n"; // 3 символа. Третий - перевод строки - -alert( str.length ); // 3 -``` - -### Доступ к символам - -Чтобы получить символ, используйте вызов `charAt(позиция)`. Первый символ имеет позицию `0`: - -```js -//+ run -var str = "jQuery"; -alert( str.charAt(0) ); // "j" -``` - -В JavaScript **нет отдельного типа "символ"**, так что `charAt` возвращает строку, состоящую из выбранного символа. - -Также для доступа к символу можно также использовать квадратные скобки: - -```js -//+ run -var str = "Я - современный браузер!"; -alert( str[0] ); // "Я" -``` - -Разница между этим способом и `charAt` заключается в том, что если символа нет -- `charAt` выдает пустую строку, а скобки -- `undefined`: - -```js -//+ run -alert( "".charAt(0) ); // пустая строка -alert( "" [0] ); // undefined -``` - -Вообще же метод `charAt` существует по историческим причинам, ведь квадратные скобки -- проще и короче. - -[warn header="Вызов метода -- всегда со скобками"] - -Обратите внимание, `str.length` -- это *свойство* строки, а `str.charAt(pos)` -- *метод*, т.е. функция. - -Обращение к методу всегда идет со скобками, а к свойству -- без скобок. - -[/warn] - - -### Изменения строк - -Содержимое строки в JavaScript нельзя изменять. Нельзя взять символ посередине и заменить его. Как только строка создана -- она такая навсегда. - -Можно лишь создать целиком новую строку и присвоить в переменную вместо старой, например: - -```js -//+ run -var str = "строка"; - -str = str[3] + str[4] + str[5]; - -alert( str ); // ока -``` - -### Смена регистра - -Методы `toLowerCase()` и `toUpperCase()` меняют регистр строки на нижний/верхний: - -```js -//+ run -alert( "Интерфейс".toUpperCase() ); // ИНТЕРФЕЙС -``` - -Пример ниже получает первый символ и приводит его к нижнему регистру: - -```js -alert( "Интерфейс" [0].toLowerCase() ); // 'и' -``` - -### Поиск подстроки - -Для поиска подстроки есть метод indexOf(подстрока[, начальная_позиция]). - -Он возвращает позицию, на которой находится `подстрока` или `-1`, если ничего не найдено. Например: - -```js -//+ run -var str = "Widget with id"; - -alert( str.indexOf("Widget") ); // 0, т.к. "Widget" найден прямо в начале str -alert( str.indexOf("id") ); // 1, т.к. "id" найден, начиная с позиции 1 -alert( str.indexOf("widget") ); // -1, не найдено, так как поиск учитывает регистр -``` - -Необязательный второй аргумент позволяет искать, начиная с указанной позиции. Например, первый раз `"id"` появляется на позиции `1`. Чтобы найти его следующее появление -- запустим поиск с позиции `2`: - -```js -//+ run -var str = "Widget with id"; - -alert(str.indexOf("id", 2)) // 12, поиск начат с позиции 2 -``` - -Также существует аналогичный метод lastIndexOf, который ищет не с начала, а с конца строки. - -[smart] -Для красивого вызова `indexOf` применяется побитовый оператор НЕ `'~'`. - -Дело в том, что вызов `~n` эквивалентен выражению `-(n+1)`, например: - -```js -//+ run -alert( ~2 ); // -(2+1) = -3 -alert( ~1 ); // -(1+1) = -2 -alert( ~0 ); // -(0+1) = -1 -*!* -alert( ~-1 ); // -(-1+1) = 0 -*/!* -``` - -Как видно, `~n` -- ноль только в случае, когда `n == -1`. - -То есть, проверка `if ( ~str.indexOf(...) )` означает, что результат `indexOf` отличен от `-1`, т.е. совпадение есть. - -Вот так: - -```js -//+ run -var str = "Widget"; - -if (~str.indexOf("get")) { - alert( 'совпадение есть!' ); -} -``` - -Вообще, использовать возможности языка неочевидным образом не рекомендуется, поскольку ухудшает читаемость кода. - -Однако, в данном случае, все в порядке. Просто запомните: `'~'` читается как "не минус один", а `"if ~str.indexOf"` читается как `"если найдено"`. - -[/smart] - - -### Поиск всех вхождений - -Чтобы найти все вхождения подстроки, нужно запустить `indexOf` в цикле. Как только получаем очередную позицию -- начинаем следующий поиск со следующей. - -Пример такого цикла: - -```js -//+ run -var str = "Ослик Иа-Иа посмотрел на виадук"; // ищем в этой строке -var target = "Иа"; // цель поиска - -var pos = 0; -while (true) { - var foundPos = str.indexOf(target, pos); - if (foundPos == -1) break; - - alert( foundPos ); // нашли на этой позиции - pos = foundPos + 1; // продолжить поиск со следующей -} -``` - -Такой цикл начинает поиск с позиции `0`, затем найдя подстроку на позиции `foundPos`, следующий поиск продолжит с позиции `pos = foundPos+1`, и так далее, пока что-то находит. - -Впрочем, тот же алгоритм можно записать и короче: - -```js -//+ run -var str = "Ослик Иа-Иа посмотрел на виадук"; // ищем в этой строке -var target = "Иа"; // цель поиска - -*!* -var pos = -1; -while ((pos = str.indexOf(target, pos + 1)) != -1) { - alert( pos ); -} -*/!* -``` - -### Взятие подстроки: substr, substring, slice. - -В JavaScript существуют целых 3 (!) метода для взятия подстроки, с небольшими отличиями между ними. - -
      -
      `substring(start [, end])` -
      -Метод `substring(start, end)` возвращает подстроку с позиции `start` до, но не включая `end`. - -```js -//+ run -var str = "*!*s*/!*tringify"; -alert(str.substring(0,1)); // "s", символы с позиции 0 по 1 не включая 1. -``` - -Если аргумент `end` отсутствует, то идет до конца строки: - -```js -//+ run -var str = "st*!*ringify*/!*"; -alert(str.substring(2)); // ringify, символы с позиции 2 до конца -``` - -
      -
      `substr(start [, length])`
      -
      Первый аргумент имеет такой же смысл, как и в `substring`, а второй содержит не конечную позицию, а количество символов. - -```js -//+ run -var str = "st*!*ring*/!*ify"; -str = str.substr(2,4); // ring, со 2й позиции 4 символа -alert(str) -``` - -Если второго аргумента нет -- подразумевается "до конца строки".
      -
      `slice(start [, end])`
      -
      Возвращает часть строки от позиции `start` до, но не включая, позиции `end`. Смысл параметров -- такой же как в `substring`.
      -
      - -### Отрицательные аргументы - -Различие между `substring` и `slice` -- в том, как они работают с отрицательными и выходящими за границу строки аргументами: - -
      -
      `substring(start, end)`
      -
      Отрицательные аргументы интерпретируются как равные нулю. Слишком большие значения усекаются до длины строки: - -```js -//+ run -alert( "testme".substring(-2) ); // "testme", -2 становится 0 -``` - -Кроме того, если start > end, то аргументы меняются местами, т.е. возвращается участок строки *между* `start` и `end`: - -```js -//+ run -alert( "testme".substring(4, -1) ); // "test" -// -1 становится 0 -> получили substring(4, 0) -// 4 > 0, так что аргументы меняются местами -> substring(0, 4) = "test" -``` - -
      -
      `slice`
      -
      Отрицательные значения отсчитываются от конца строки: - -```js -//+ run -alert( "testme".slice(-2) ); // "me", от 2 позиции с конца -``` - - - -```js -//+ run -alert( "testme".slice(1, -1) ); // "estm", от 1 позиции до первой с конца. -``` - -Это гораздо более удобно, чем странная логика `substring`. -
      -
      - -Отрицательное значение первого параметра поддерживается в `substr` во всех браузерах, кроме IE8-. - -Если выбирать из этих трёх методов один, для использования в большинстве ситуаций -- то это будет `slice`: он и отрицательные аргументы поддерживает и работает наиболее очевидно. - -## Кодировка Юникод - -Как мы знаем, символы сравниваются в алфавитном порядке `'А' < 'Б' < 'В' < ... < 'Я'`. - -Но есть несколько странностей.. - -
        -
      1. Почему буква `'а'` маленькая больше буквы `'Я'` большой? - -```js -//+ run -alert( 'а' > 'Я' ); // true -``` - -
      2. -Буква `'ё'` находится в алфавите между `е` и `ж`: абвгде**ё**жз... Но почему тогда `'ё'` больше `'я'`? - -```js -//+ run -alert( 'ё' > 'я' ); // true -``` - -
      3. -
      - -Чтобы разобраться с этим, обратимся к внутреннему представлению строк в JavaScript. - -**Все строки имеют внутреннюю кодировку [Юникод](http://ru.wikipedia.org/wiki/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4).** - -Неважно, на каком языке написана страница, находится ли она в windows-1251 или utf-8. Внутри JavaScript-интерпретатора все строки приводятся к единому "юникодному" виду. Каждому символу соответствует свой код. - -Есть метод для получения символа по его коду: -
      -
      String.fromCharCode(code)
      -
      Возвращает символ по коду `code`: - -```js -//+ run -alert( String.fromCharCode(1072) ); // 'а' -``` - -
      -
      - -...И метод для получения цифрового кода из символа: - -
      -
      str.charCodeAt(pos)
      -
      Возвращает код символа на позиции `pos`. Отсчет позиции начинается с нуля. - -```js -//+ run -alert( "абрикос".charCodeAt(0) ); // 1072, код 'а' -``` - -
      -
      - -Теперь вернемся к примерам выше. Почему сравнения `'ё' > 'я'` и `'а' > 'Я'` дают такой странный результат? - -Дело в том, что **символы сравниваются не по алфавиту, а по коду**. У кого код больше -- тот и больше. В юникоде есть много разных символов. Кириллическим буквам соответствует только небольшая часть из них, подробнее -- [Кириллица в Юникоде](http://ru.wikipedia.org/wiki/%D0%9A%D0%B8%D1%80%D0%B8%D0%BB%D0%BB%D0%B8%D1%86%D0%B0_%D0%B2_%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4%D0%B5). - -Выведем отрезок символов юникода с кодами от `1034` до `1113`: - -```js -//+ run -var str = ''; -for (var i = 1034; i <= 1113; i++) { - str += String.fromCharCode(i); -} -alert( str ); -``` - -Результат: -
      -ЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяѐёђѓєѕіїјљ -
      - -Мы можем увидеть из этого отрезка две важных вещи: - -
        -
      1. **Строчные буквы идут после заглавных, поэтому они всегда больше.** - -В частности, `'а'(код 1072) > 'Я'(код 1071)`. - -То же самое происходит и в английском алфавите, там `'a' > 'Z'`. -
      2. -
      3. **Ряд букв, например `ё`, находятся вне основного алфавита.** - -В частности, маленькая буква `ё` имеет код, больший чем `я`, поэтому **`'ё'(код 1105) > 'я'(код 1103)`**. - -Кстати, большая буква `Ё` располагается в Unicode до `А`, поэтому **`'Ё'`(код 1025) < `'А'`(код 1040)**. Удивительно: есть буква меньше чем `А` :) -
      4. -
      - -**Буква `ё` не уникальна, точки над буквой используются и в других языках, приводя к тому же результату.** - -Например, при работе с немецкими названиями: - -```js -//+ run -alert( "ö" > "z" ); // true -``` - -[smart header="Юникод в HTML"] -Кстати, если мы знаем код символа в кодировке юникод, то можем добавить его в HTML, используя "числовую ссылку" (numeric character reference). - -Для этого нужно написать сначала `&#`, затем код, и завершить точкой с запятой `';'`. Например, символ `'а'` в виде числовой ссылки: `а`. - -Если код хотят дать в 16-ричной системе счисления, то начинают с `&#x`. - -В юникоде есть много забавных и полезных символов, например, символ ножниц: ✂ (`✂`), дроби: ½ (`½`) ¾ (`¾`) и другие. Их можно использовать вместо картинок в дизайне. -[/smart] - - -## Посимвольное сравнение - -Сравнение строк работает *лексикографически*, иначе говоря, посимвольно. - -Сравнение строк `s1` и `s2` обрабатывается по следующему алгоритму: - -
      1. Сравниваются первые символы: `s1[0]` и `s2[0]`. Если они разные, то сравниваем их и, в зависимости от результата их сравнения, возвратить `true` или `false`. Если же они одинаковые, то...
      2. -
      3. Сравниваются вторые символы `s1[1]` и `s2[1]`
      4. -
      5. Затем третьи `s1[2]` и `s2[2]` и так далее, пока символы не будут наконец разными, и тогда какой символ больше -- та строка и больше. Если же в какой-либо строке закончились символы, то считаем, что она меньше, а если закончились в обеих -- они равны.
      6. -
      - -Спецификация языка определяет этот алгоритм более детально. Если же говорить простыми словами, смысл алгоритма в точности соответствует порядку, по которому имена заносятся в орфографический словарь. - -```js -"Вася" > "Ваня" // true, т.к. начальные символы совпадают, а потом 'с' > 'н' -"Дома" > "До" // true, т.к. начало совпадает, но в 1й строке больше символов -``` - -[warn header="Числа в виде строк сравниваются как строки"] - -Бывает, что числа приходят в скрипт в виде строк, например как результат `prompt`. В этом случае результат их сравнения будет неверным: - -```js -//+ run -alert( "2" > "14" ); // true, так как это строки, и для первых символов верно "2" > "1" -``` - -Если хотя бы один аргумент -- не строка, то другой будет преобразован к числу: - -```js -//+ run -alert( 2 > "14" ); // false -``` - -[/warn] - -## Правильное сравнение - -Все современные браузеры, кроме IE10- (для которых нужно подключить библиотеку [Intl.JS](https://github.com/andyearnshaw/Intl.js/)) поддерживают стандарт [ECMA 402](http://www.ecma-international.org/ecma-402/1.0/ECMA-402.pdf), поддерживающий сравнение строк на разных языках, с учётом их правил. - -Способ использования: - -```js -//+ run -var str = "Ёлки"; - -alert( str.localeCompare("Яблони") ); // -1 -``` - -Метод `str1.localeCompare(str2)` возвращает `-1`, если `str1 < str2`, `1`, если `str1 > str2` и `0`, если они равны. - -Более подробно про устройство этого метода можно будет узнать в статье [](/intl), когда это вам понадобится. - -## Итого - -
        -
      • Строки в JavaScript имеют внутреннюю кодировку Юникод. При написании строки можно использовать специальные символы, например `\n` и вставлять юникодные символы по коду.
      • -
      • Мы познакомились со свойством `length` и методами `charAt`, `toLowerCase/toUpperCase`, `substring/substr/slice` (предпочтителен `slice`). Есть и другие методы, например [trim](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim) обрезает пробелы с начала и конца строки.
      • -
      • Строки сравниваются побуквенно. Поэтому если число получено в виде строки, то такие числа могут сравниваться некорректно, нужно преобразовать его к типу *number*.
      • -
      • При сравнении строк следует иметь в виду, что буквы сравниваются по их кодам. Поэтому большая буква меньше маленькой, а буква `ё` вообще вне основного алфавита.
      • -
      • Для правильного сравнения существует целый стандарт ECMA 402. Это не такое простое дело, много языков и много правил. Он поддерживается во всех современных браузерах, кроме IE10-, в которых нужна библиотека [](https://github.com/andyearnshaw/Intl.js/). Такое сравнение работает через вызов `str1.localeCompare(str2)`.
      • -
      - -Больше информации о методах для строк можно получить в справочнике: [http://javascript.ru/String](). \ No newline at end of file diff --git a/1-js/4-data-structures/4-object/1-hello-object/solution.md b/1-js/4-data-structures/4-object/1-hello-object/solution.md deleted file mode 100644 index 465bf1ad..00000000 --- a/1-js/4-data-structures/4-object/1-hello-object/solution.md +++ /dev/null @@ -1,10 +0,0 @@ - - -```js -var user = {}; -user.name = "Вася"; -user.surname = "Петров"; -user.name = "Сергей"; -delete user.name; -``` - diff --git a/1-js/4-data-structures/4-object/1-hello-object/task.md b/1-js/4-data-structures/4-object/1-hello-object/task.md deleted file mode 100644 index b0dfdaa3..00000000 --- a/1-js/4-data-structures/4-object/1-hello-object/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Первый объект - -[importance 3] - -Мини-задача на синтаксис объектов. Напишите код, по строке на каждое действие. -
        -
      1. Создайте пустой объект `user`.
      2. -
      3. Добавьте свойство `name` со значением `Вася`.
      4. -
      5. Добавьте свойство `surname` со значением `Петров`.
      6. -
      7. Поменяйте значение `name` на `Сергей`.
      8. -
      9. Удалите свойство `name` из объекта.
      10. -
      - diff --git a/1-js/4-data-structures/4-object/article.md b/1-js/4-data-structures/4-object/article.md deleted file mode 100644 index 862ebc46..00000000 --- a/1-js/4-data-structures/4-object/article.md +++ /dev/null @@ -1,339 +0,0 @@ -# Объекты как ассоциативные массивы - -Объекты в JavaScript сочетают в себе два важных функционала. - -Первый -- это ассоциативный массив: структура, пригодная для хранения любых данных. В этой главе мы рассмотрим использование объектов именно как массивов. - -Второй -- языковые возможности для объектно-ориентированного программирования. Эти возможности мы изучим в последующих разделах учебника. - -[cut] -## Ассоциативные массивы - -[Ассоциативный массив](http://ru.wikipedia.org/wiki/%D0%90%D1%81%D1%81%D0%BE%D1%86%D0%B8%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D1%8B%D0%B9_%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2) -- структура данных, в которой можно хранить любые данные в формате ключ-значение. - -Её можно легко представить как шкаф с подписанными ящиками. Все данные хранятся в ящичках. По имени можно легко найти ящик и взять то значение, которое в нём лежит. - - - -В отличие от реальных шкафов, в ассоциативный массив можно в любой момент добавить новые именованные "ящики" или удалить существующие. Далее мы увидим примеры, как это делается. - -Кстати, в других языках программирования такую структуру данных также называют *"словарь"* и *"хэш"*. - -## Создание объектов - -Пустой объект ("пустой шкаф") может быть создан одним из двух синтаксисов: - -```js -1. o = new Object(); -2. o = {}; // пустые фигурные скобки -``` - -Обычно все пользуются синтаксисом `(2)`, т.к. он короче. - -## Операции с объектом - -Объект может содержать в себе любые значения, которые называются *свойствами объекта*. Доступ к свойствам осуществляется по *имени свойства* (иногда говорят *"по ключу"*). - -Например, создадим объект `person` для хранения информации о человеке: - -```js -var person = {}; // пока пустой -``` - - - - -Основные операции с объектами -- это создание, получение и удаление свойств. - -Для обращения к свойствам используется запись "через точку", вида `объект.свойство`, например: - -```js -// при присвоении свойства в объекте автоматически создаётся "ящик" -// с именем "name" и в него записывается содержимое 'Вася' -person.name = 'Вася'; - -person.age = 25; // запишем ещё одно свойство: с именем 'age' и значением 25 -``` - - - -Значения хранятся "внутри" ящиков. Обратим внимание -- любые значения, любых типов: число, строка -- не важно. - -Чтобы прочитать их -- также обратимся через точку: -```js -alert( person.name + ': ' + person.age ); // "Вася: 25" -``` - -Удаление осуществляется оператором `delete`: - -```js -delete person.age; -``` - -Осталось только свойство `name`: - - - -Следующая операция: -
        -
      1. **Проверка существования свойства с определенным ключом.**
      2. -
      - -Для проверки существования свойства в объекте есть оператор `in`. - -Его синтаксис: `"prop" in obj`, причем имя свойства -- в виде строки, например: - -```js -if ("name" in person) { - alert( "Свойство name существует!" ); -} -``` - -Впрочем, чаще используется другой способ -- сравнение значения с `undefined`. - -Дело в том, что **в JavaScript можно обратиться к любому свойству объекта, даже если его нет**. Ошибки не будет. - -Но если свойство не существует, то вернется специальное значение `undefined`: - -```js -//+ run -var person = {}; - -alert( person.lalala ); // undefined, нет свойства с ключом lalala -``` - -Таким образом **мы можем легко проверить существование свойства -- получив его и сравнив с `undefined`**: - -```js -//+ run -var person = { - name: "Василий" -}; - -alert( person.lalala === undefined ); // true, свойства нет -alert( person.name === undefined ); // false, свойство есть. -``` - -[smart header="Разница между проверками `in` и `=== undefined`"] - -Есть два средства для проверки наличия свойства в объекте: первое -- оператор `in`, второе -- получить его и сравнить его с `undefined`. - -Они почти идентичны, но есть одна небольшая разница. - -Дело в том, что технически возможно, что *свойство есть и равно `undefined`*: - -```js -//+ untrusted refresh run -var obj = {}; -obj.test = undefined; // добавили свойство со значением undefined - -*!* -// проверим наличие свойств test и заведомо отсутствующего blabla -alert( obj.test === undefined ); // true -alert( obj.blabla === undefined ); // true -*/!* -``` - -...При этом, как видно из кода, при простом сравнении наличие такого свойства будет неотличимо от его отсутствия. - -Но оператор `in` гарантирует правильный результат: - -```js -//+ untrusted refresh run -var obj = {}; -obj.test = undefined; - -*!* -alert( "test" in obj ); // true -alert( "blabla" in obj ); // false -*/!* -``` - -Как правило, в коде мы не будем присваивать `undefined`, чтобы корректно работали обе проверки. А в качестве значения, обозначающего неизвестность и неопределенность, будем использовать `null`. -[/smart] - -### Доступ через квадратные скобки - -Существует альтернативный синтаксис работы со свойствами, использующий квадратные скобки `объект['свойство']`: - -```js -//+ run -var person = {}; - -person['name'] = 'Вася'; // то же что и person.name = 'Вася' -``` - -Записи `person['name']` и `person.name` идентичны, но квадратные скобки позволяют использовать в качестве имени свойства любую строку: - -```js -//+ run -var person = {}; - -person['любимый стиль музыки'] = 'Джаз'; // то же что и person.name = 'Вася' -``` - -Такое присвоение было бы невозможно "через точку", так интерпретатор после первого пробела подумает, что свойство закончилось, и далее выдаст ошибку: - -```js -//+ run -person.любимый стиль музыки = 'Джаз'; // ??? ошибка -``` - -В обоих случаях, **имя свойства обязано быть строкой**. Если использовано значение другого типа -- JavaScript приведет его к строке автоматически. - -### Доступ к свойству через переменную - -Квадратные скобки также позволяют обратиться к свойству, имя которого хранится в переменной: - -```js -//+ run -var person = { - age: 25 -}; -var key = 'age'; - -alert( person[key] ); // выведет person['age'] -``` - -Вообще, если имя свойства хранится в переменной (`var key = "age"`), то единственный способ к нему обратиться -- это квадратные скобки `person[key]`. - -Доступ через точку используется, если мы на этапе написания программы уже знаем название свойства. А если оно будет определено по ходу выполнения, например, введено посетителем и записано в переменную, то единственный выбор -- квадратные скобки. - -### Объявление со свойствами - -Объект можно заполнить значениями при создании, указав их в фигурных скобках: `{ ключ1: значение1, ключ2: значение2, ... }`. - -Такой синтаксис называется *литеральным* (англ. literal). - -Следующие два фрагмента кода создают одинаковый объект: - -```js -var menuSetup = { - width: 300, - height: 200, - title: "Menu" -}; - -// то же самое, что: - -var menuSetup = {}; -menuSetup.width = 300; -menuSetup.height = 200; -menuSetup.title = 'Menu'; -``` - -Названия свойств можно перечислять как в кавычках, так и без, если они удовлетворяют ограничениям для имён переменных. - -Например: - -```js -var menuSetup = { - width: 300, - 'height': 200, - "мама мыла раму": true -}; -``` - -В качестве значения можно тут же указать и другой объект: - -```js -var user = { - name: "Таня", - age: 25, -*!* - size: { - top: 90, - middle: 60, - bottom: 90 - } -*/!* -} - -alert(user.name) // "Таня" - -alert(user.size.top) // 90 -``` - -Здесь значением свойства `size` является объект `{top: 90, middle: 60, bottom: 90 }`. -## Компактное представление объектов - -[warn header="Hardcore coders only"] -Эта секция относится ко внутреннему устройству структуры данных. Она не обязательна к прочтению. -[/warn] - -Браузер использует специальное "компактное" представление объектов, чтобы сэкономить память в том случае, когда однотипных объектов много. - -Например, посмотрим на такой объект: - -```js -var user = { - name: "Vasya", - age: 25 -}; -``` - -Здесь содержится информация о свойстве `name` и его строковом значении, а также о свойстве `age` и его численном значении. Представим, что таких объектов много. - -Получится, что информация об именах свойств `name` и `age` дублируется в каждом объекте. Чтобы этого избежать, браузер применяет оптимизацию. - -**При создании множества объектов одного и того же вида (с одинаковыми полями) интерпретатор выносит описание полей в отдельную структуру. А сам объект остаётся в виде непрерывной области памяти с данными.** - -Например, есть много объектов с полями `name` и `age`: - -```js -//+ no-beautify -{name: "Вася", age: 25} -{name: "Петя", age: 22} -{name: "Маша", age: 19} -... -``` - -Для их эффективного хранения будет создана структура, которая описывает данный вид объектов. Выглядеть она будет примерно так: ``. А сами объекты будут представлены в памяти только данными: - -```js -//+ no-beautify -<структура: string name, number age> -Вася 25 -Петя 22 -Маша 19 -``` - -При добавлении нового объекта такой структуры достаточно хранить значения полей, но не их имена. Экономия памяти -- налицо. - -А что происходит, если к объекту добавляется новое свойство? Например, к одному из них добавили свойство `isAdmin`: - -```js -user.isAdmin = true; -``` - -В этом случае браузер смотрит, есть ли уже структура, под которую подходит такой объект. Если нет -- она создаётся и объект привязывается к ней. - -**Эта оптимизация является примером того, что далеко не всё то, что мы пишем, один-в-один переносится в память.** - -Современные интерпретаторы очень стараются оптимизировать как код, так и структуры данных. Детали применения и реализации этого способа хранения варьируются от браузера к браузеру. О том, как это сделано в Chrome можно узнать, например, из презентации [Know Your Engines](http://www.slideshare.net/newmovie/know-yourengines-velocity2011). Она была некоторое время назад, но с тех пор мало что изменилось. - - -## Итого - -Объекты -- это ассоциативные массивы с дополнительными возможностями: - -
        -
      • Доступ к элементам осуществляется: -
          -
        • Напрямую по ключу `obj.prop = 5`
        • -
        • Через переменную, в которой хранится ключ: - -```js -var key = "prop"; -obj[key] = 5 -``` - -
        • -
        -
      • Удаление ключей: `delete obj.name`.
      • -
      • Существование свойства может проверять оператор `in`: `if ("prop" in obj)`, как правило, работает и просто сравнение `if (obj.prop !== undefined)`.
      • -
      - - - - diff --git a/1-js/4-data-structures/4-object/object-person-1.png b/1-js/4-data-structures/4-object/object-person-1.png deleted file mode 100644 index 532330e0..00000000 Binary files a/1-js/4-data-structures/4-object/object-person-1.png and /dev/null differ diff --git a/1-js/4-data-structures/4-object/object-person-1@2x.png b/1-js/4-data-structures/4-object/object-person-1@2x.png deleted file mode 100644 index 7904d95c..00000000 Binary files a/1-js/4-data-structures/4-object/object-person-1@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/4-object/object-person-2.png b/1-js/4-data-structures/4-object/object-person-2.png deleted file mode 100644 index 4630054b..00000000 Binary files a/1-js/4-data-structures/4-object/object-person-2.png and /dev/null differ diff --git a/1-js/4-data-structures/4-object/object-person-2@2x.png b/1-js/4-data-structures/4-object/object-person-2@2x.png deleted file mode 100644 index 73ab98e8..00000000 Binary files a/1-js/4-data-structures/4-object/object-person-2@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/4-object/object-person-empty.png b/1-js/4-data-structures/4-object/object-person-empty.png deleted file mode 100644 index 668f13bf..00000000 Binary files a/1-js/4-data-structures/4-object/object-person-empty.png and /dev/null differ diff --git a/1-js/4-data-structures/4-object/object-person-empty@2x.png b/1-js/4-data-structures/4-object/object-person-empty@2x.png deleted file mode 100644 index da71618c..00000000 Binary files a/1-js/4-data-structures/4-object/object-person-empty@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/4-object/object.png b/1-js/4-data-structures/4-object/object.png deleted file mode 100644 index abda01bb..00000000 Binary files a/1-js/4-data-structures/4-object/object.png and /dev/null differ diff --git a/1-js/4-data-structures/4-object/object@2x.png b/1-js/4-data-structures/4-object/object@2x.png deleted file mode 100644 index bd2590bb..00000000 Binary files a/1-js/4-data-structures/4-object/object@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/5-object-for-in/1-is-empty/_js.view/solution.js b/1-js/4-data-structures/5-object-for-in/1-is-empty/_js.view/solution.js deleted file mode 100644 index 7ce59096..00000000 --- a/1-js/4-data-structures/5-object-for-in/1-is-empty/_js.view/solution.js +++ /dev/null @@ -1,6 +0,0 @@ -function isEmpty(obj) { - for (var key in obj) { - return false; - } - return true; -} \ No newline at end of file diff --git a/1-js/4-data-structures/5-object-for-in/1-is-empty/_js.view/test.js b/1-js/4-data-structures/5-object-for-in/1-is-empty/_js.view/test.js deleted file mode 100644 index ca9ec19a..00000000 --- a/1-js/4-data-structures/5-object-for-in/1-is-empty/_js.view/test.js +++ /dev/null @@ -1,11 +0,0 @@ -describe("isEmpty", function() { - it("если объект пустой - возвращает true", function() { - assert.isTrue(isEmpty({})); - }); - - it("если у объекта есть любое свойство, не важно какое - возвращает false", function() { - assert.isFalse(isEmpty({ - anything: false - })); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/5-object-for-in/1-is-empty/solution.md b/1-js/4-data-structures/5-object-for-in/1-is-empty/solution.md deleted file mode 100644 index 324b761e..00000000 --- a/1-js/4-data-structures/5-object-for-in/1-is-empty/solution.md +++ /dev/null @@ -1,20 +0,0 @@ - - -```js -//+ run -function isEmpty(obj) { - for (var key in obj) { - return false; - } - return true; -} - -var schedule = {}; - -alert( isEmpty(schedule) ); // true - -schedule["8:30"] = "подъём"; - -alert( isEmpty(schedule) ); // false -``` - diff --git a/1-js/4-data-structures/5-object-for-in/1-is-empty/task.md b/1-js/4-data-structures/5-object-for-in/1-is-empty/task.md deleted file mode 100644 index 6a5a6760..00000000 --- a/1-js/4-data-structures/5-object-for-in/1-is-empty/task.md +++ /dev/null @@ -1,22 +0,0 @@ -# Определите, пуст ли объект - -[importance 5] - -Создайте функцию `isEmpty(obj)`, которая возвращает `true`, если в объекте нет свойств и `false` -- если хоть одно свойство есть. - -Работать должно так: - -```js -function isEmpty(obj) { - /* ваш код */ -} - -var schedule = {}; - -alert( isEmpty(schedule) ); // true - -schedule["8:30"] = "подъём"; - -alert( isEmpty(schedule) ); // false -``` - diff --git a/1-js/4-data-structures/5-object-for-in/2-sum-salaries/solution.md b/1-js/4-data-structures/5-object-for-in/2-sum-salaries/solution.md deleted file mode 100644 index 4c07e961..00000000 --- a/1-js/4-data-structures/5-object-for-in/2-sum-salaries/solution.md +++ /dev/null @@ -1,20 +0,0 @@ - - -```js -//+ run -"use strict"; - -var salaries = { - "Вася": 100, - "Петя": 300, - "Даша": 250 -}; - -var sum = 0; -for (var name in salaries) { - sum += salaries[name]; -} - -alert( sum ); -``` - diff --git a/1-js/4-data-structures/5-object-for-in/2-sum-salaries/task.md b/1-js/4-data-structures/5-object-for-in/2-sum-salaries/task.md deleted file mode 100644 index 9a321fad..00000000 --- a/1-js/4-data-structures/5-object-for-in/2-sum-salaries/task.md +++ /dev/null @@ -1,23 +0,0 @@ -# Сумма свойств - -[importance 5] - -Есть объект `salaries` с зарплатами. Напишите код, который выведет сумму всех зарплат. - -Если объект пустой, то результат должен быть `0`. - -Например: - -```js -"use strict"; - -var salaries = { - "Вася": 100, - "Петя": 300, - "Даша": 250 -}; - -//... ваш код выведет 650 -``` - -P.S. Сверху стоит `use strict`, чтобы не забыть объявить переменные. \ No newline at end of file diff --git a/1-js/4-data-structures/5-object-for-in/3-max-salary/solution.md b/1-js/4-data-structures/5-object-for-in/3-max-salary/solution.md deleted file mode 100644 index 24d7f1df..00000000 --- a/1-js/4-data-structures/5-object-for-in/3-max-salary/solution.md +++ /dev/null @@ -1,24 +0,0 @@ - - -```js -//+ run -"use strict"; - -var salaries = { - "Вася": 100, - "Петя": 300, - "Даша": 250 -}; - -var max = 0; -var maxName = ""; -for (var name in salaries) { - if (max < salaries[name]) { - max = salaries[name]; - maxName = name; - } -} - -alert( maxName || "нет сотрудников" ); -``` - diff --git a/1-js/4-data-structures/5-object-for-in/3-max-salary/task.md b/1-js/4-data-structures/5-object-for-in/3-max-salary/task.md deleted file mode 100644 index 86264eb0..00000000 --- a/1-js/4-data-structures/5-object-for-in/3-max-salary/task.md +++ /dev/null @@ -1,22 +0,0 @@ -# Свойство с наибольшим значением - -[importance 5] - -Есть объект `salaries` с зарплатами. Напишите код, который выведет имя сотрудника, у которого самая большая зарплата. - -Если объект пустой, то пусть он выводит "нет сотрудников". - -Например: - -```js -"use strict"; - -var salaries = { - "Вася": 100, - "Петя": 300, - "Даша": 250 -}; - -// ... ваш код выведет "Петя" -``` - diff --git a/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/_js.view/solution.js b/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/_js.view/solution.js deleted file mode 100644 index 799afaef..00000000 --- a/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/_js.view/solution.js +++ /dev/null @@ -1,11 +0,0 @@ -function isNumeric(n) { - return !isNaN(parseFloat(n)) && isFinite(n) -} - -function multiplyNumeric(obj) { - for (var key in obj) { - if (isNumeric(obj[key])) { - obj[key] *= 2; - } - } -} \ No newline at end of file diff --git a/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/_js.view/source.js b/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/_js.view/source.js deleted file mode 100644 index 7caae25d..00000000 --- a/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/_js.view/source.js +++ /dev/null @@ -1,5 +0,0 @@ -function isNumeric(n) { - return !isNaN(parseFloat(n)) && isFinite(n) -} - -// ... ваш код ... \ No newline at end of file diff --git a/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/_js.view/test.js b/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/_js.view/test.js deleted file mode 100644 index 73a1c465..00000000 --- a/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/_js.view/test.js +++ /dev/null @@ -1,13 +0,0 @@ -describe("multiplyNumeric", function() { - it("умножает численные свойства на 2", function() { - var menu = { - width: 200, - height: "300", - title: "Моё меню" - }; - multiplyNumeric(menu); - assert.equal(menu.width, 400); - assert.equal(menu.height, 600); - assert.equal(menu.title, "Моё меню"); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/solution.md b/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/solution.md deleted file mode 100644 index e42d8c9d..00000000 --- a/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/solution.md +++ /dev/null @@ -1,27 +0,0 @@ - - -```js -//+ run -var menu = { - width: 200, - height: 300, - title: "My menu" -}; - -function isNumeric(n) { - return !isNaN(parseFloat(n)) && isFinite(n); -} - -function multiplyNumeric(obj) { - for (var key in obj) { - if (isNumeric(obj[key])) { - obj[key] *= 2; - } - } -} - -multiplyNumeric(menu); - -alert( "menu width=" + menu.width + " height=" + menu.height + " title=" + menu.title ); -``` - diff --git a/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/task.md b/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/task.md deleted file mode 100644 index 48ee5c2a..00000000 --- a/1-js/4-data-structures/5-object-for-in/4-multiply-numeric/task.md +++ /dev/null @@ -1,32 +0,0 @@ -# Умножьте численные свойства на 2 - -[importance 3] - -Создайте функцию `multiplyNumeric`, которая получает объект и умножает все численные свойства на 2. Например: - -```js -// до вызова -var menu = { - width: 200, - height: 300, - title: "My menu" -}; - -multiplyNumeric(menu); - -// после вызова -menu = { - width: 400, - height: 600, - title: "My menu" -}; -``` - -P.S. Для проверки на число используйте функцию: - -```js -function isNumeric(n) { - return !isNaN(parseFloat(n)) && isFinite(n) -} -``` - diff --git a/1-js/4-data-structures/5-object-for-in/article.md b/1-js/4-data-structures/5-object-for-in/article.md deleted file mode 100644 index 8418af31..00000000 --- a/1-js/4-data-structures/5-object-for-in/article.md +++ /dev/null @@ -1,171 +0,0 @@ -# Объекты: перебор свойств - -Для перебора всех свойств из объекта используется цикл по свойствам `for..in`. Эта синтаксическая конструкция отличается от рассмотренного ранее цикла `for(;;)`. - -[cut] - -## for..in [#for..in] - -Синтаксис: - -```js -for (key in obj) { - /* ... делать что-то с obj[key] ... */ -} -``` - -При этом `for..in` последовательно переберёт свойства объекта `obj`, имя каждого свойства будет записано в `key` и вызвано тело цикла. - -[smart header="Объявление переменной в цикле `for (var key in obj)`"] -Вспомогательную переменную `key` можно объявить прямо в цикле: - -```js -for (*!*var key*/!* in menu) { - // ... -} -``` - -Так иногда пишут для краткости кода. Можно использовать и любое другое название, кроме `key`, например `for(var propName in menu)`. -[/smart] - -Пример итерации по свойствам: - -```js -//+ run -var menu = { - width: 300, - height: 200, - title: "Menu" -}; - -for (var key in menu) { - // этот код будет вызван для каждого свойства объекта - // ..и выведет имя свойства и его значение - -*!* - alert( "Ключ: " + key + " значение:" + menu[key] ); -*/!* -} -``` - -Обратите внимание, мы использовали квадратные скобки `menu[key]`. Как уже говорилось, если имя свойства хранится в переменной, то обратиться к нему можно только так, не через точку. - -## Количество свойств в объекте - -Как узнать, сколько свойств хранит объект? - -Готового метода для этого нет. - -Самый кросс-браузерный способ -- это сделать цикл по свойствам и посчитать, вот так: - -```js -//+ run -var menu = { - width: 300, - height: 200, - title: "Menu" -}; - -*!* -var counter = 0; - -for (var key in menu) { - counter++; -} -*/!* - -alert( "Всего свойств: " + counter ); -``` - -В следующих главах мы пройдём массивы и познакомимся с другим, более коротким, вызовом: `Object.keys(menu).length`. - -## В каком порядке перебираются свойства? - -Для примера, рассмотрим объект, который задаёт список опций для выбора страны: - -```js -var codes = { - // телефонные коды в формате "код страны": "название" - "7": "Россия", - "38": "Украина", - // .., - "1": "США" -}; -``` - -Здесь мы предполагаем, что большинство посетителей из России, и поэтому начинаем с `7`, это зависит от проекта. - -При выборе телефонного кода мы хотели бы предлагать варианты, начиная с первого. Обычно на основе списка генерируется `select`, но здесь нам важно не это, а важно другое. - -**Правда ли, что при переборе `for(key in codes)` ключи `key` будут перечислены именно в том порядке, в котором заданы?** - -**По стандарту -- нет. Но некоторое соглашение об этом, всё же, есть.** - -Соглашение говорит, что если имя свойства -- нечисловая строка, то такие ключи всегда перебираются в том же порядке. Так получилось по историческим причинам и изменить это сложно: поломается много готового кода. - -С другой стороны, если имя свойства -- число, то все современные браузеры сортируют такие свойства в целях внутренней оптимизации. - -К примеру, рассмотрим объект с заведомо нечисловыми свойствами: - -```js -//+ run -var user = { - name: "Вася", - surname: "Петров" -}; -user.age = 25; - -*!* -// порядок перебора соответствует порядку присвоения свойства -*/!* -for (var prop in user) { - alert( prop ); // name, surname, age -} -``` - -А теперь -- что будет, если перебрать объект с кодами? - -```js -//+ run -var codes = { - // телефонные коды в формате "код страны": "название" - "7": "Россия", - "38": "Украина", - "1": "США" -}; - -for (var code in codes) alert( code ); // 1, 7, 38 -``` - -При запуске этого кода в современном браузере мы увидим, что на первое место попал код США! - -Нарушение порядка возникло, потому что ключи численные. Интерпретатор JavaScript видит, что строка на самом деле является числом и преобразует ключ в немного другой внутренний формат. Дополнительным эффектом внутренних оптимизаций является сортировка. - -**А что, если мы хотим, чтобы порядок был именно таким, какой мы задали?** - -Это возможно. Можно применить небольшой хак, который заключается в том, чтобы сделать все ключи нечисловыми, например, добавим в начало дополнительный символ `'+'`: - -```js -//+ run -var codes = { - "+7": "Россия", - "+38": "Украина", - "+1": "США" -}; - -for (var code in codes) { - var value = codes[code]; - code = +code; // ..если нам нужно именно число, преобразуем: "+7" -> 7 - - alert( code + ": " + value ); // 7, 38, 1 во всех браузерах -} -``` - -## Итого - -
        -
      • Цикл по ключам: `for (key in obj)`.
      • -
      • Порядок перебора соответствует порядку объявления для нечисловых ключей, а числовые -- сортируются (в современных браузерах).
      • -
      • Если нужно, чтобы порядок перебора числовых ключей соответствовал их объявлению в объекте, то используют трюк: числовые ключи заменяют на похожие, но содержащие не только цифры. Например, добавляют в начало `+`, как описано в примере выше, а потом, в процессе обработки, преобразуют такие ключи в числа.
      • -
      - diff --git a/1-js/4-data-structures/6-object-reference/article.md b/1-js/4-data-structures/6-object-reference/article.md deleted file mode 100644 index 76d9b028..00000000 --- a/1-js/4-data-structures/6-object-reference/article.md +++ /dev/null @@ -1,157 +0,0 @@ -# Объекты: передача по ссылке - -Фундаментальным отличием объектов от примитивов, является их хранение и копирование "по ссылке". - -[cut] - -## Копирование по значению - -Обычные значения: строки, числа, булевы значения, `null/undefined` при присваивании переменных копируются целиком или, как говорят, *"по значению"*. - -```js -var message = "Привет"; -var phrase = message; -``` - -В результате такого копирования получились две полностью независимые переменные, в каждой из которых хранится значение `"Привет"`. - - - -## Копирование по ссылке - -С объектами -- всё не так. - -**В переменной, которой присвоен объект, хранится не сам объект, а "адрес его места в памяти", иными словами -- "ссылка" на него.** - -Вот как выглядит переменная, которой присвоен объект: - -```js -var user = { - name: "Вася" -}; -``` - - - -Внимание: объект -- вне переменной. В переменной -- лишь "адрес" (ссылка) для него. - -**При копировании переменной с объектом -- копируется эта ссылка, а объект по-прежнему остается в единственном экземпляре.** - -Например: - -```js -//+ no-beautify -var user = { name: "Вася" }; // в переменной - ссылка - -var admin = user; // скопировали ссылку -``` - -Получили две переменные, в которых находятся ссылки на один и тот же объект: - - - -**Так как объект всего один, то изменения через любую переменную видны в других переменных:** - -```js -//+ run -var user = { name: 'Вася' }; - -var admin = user; - -*!*admin.name*/!* = 'Петя'; // поменяли данные через admin - -alert(*!*user.name*/!*); // 'Петя', изменения видны в user -``` - -[smart header="Переменная с объектом как \"ключ\" к сейфу с данными"] -Ещё одна аналогия: переменная, в которую присвоен объект, на самом деле хранит не сами данные, а ключ к сейфу, где они хранятся. - -При копировании её, получается что мы сделали копию ключа, но сейф по-прежнему один. -[/smart] - -## Клонирование объектов - -Иногда, на практике -- очень редко, нужно скопировать объект целиком, создать именно полную независимую копию, "клон" объекта. - -Что ж, можно сделать и это. Для этого нужно пройти по объекту, достать данные и скопировать на уровне примитивов. - -Примерно так: - -```js -//+ run -var user = { - name: "Вася", - age: 30 -}; - -*!* -var clone = {}; // новый пустой объект - -// скопируем в него все свойства user -for (var key in user) { - clone[key] = user[key]; -} -*/!* - -// теперь clone - полностью независимая копия -clone.name = "Петя"; // поменяли данные в clone - -alert( user.name ); // по-прежнем "Вася" -``` - -В этом коде каждое свойство объекта `user` копируется в `clone`. Если предположить, что они примитивны, то каждое скопируется по значению и мы как раз получим полный клон. - -Если же свойства объектов, в свою очередь, могут хранить ссылки на другие объекты, то нужно обойти такие подобъекты и тоже склонировать их. Это называют "глубоким" клонированием. - -## Вывод в консоли - -Откройте консоль браузера (обычно [key F12]) и запустите следующий код: - -```js -//+ run -var time = { - year: 2345, - month: 11, - day: 10, - hour: 11, - minute: 12, - second: 13, - microsecond: 123456 -} - -console.log(time); // (*) -time.microsecond++; // (**) - -console.log(time); -time.microsecond++; - -console.log(time); -time.microsecond++; -``` - -Как видно, в нём некий объект выводится строкой `(*)`, затем он меняется в строке `(**)` и снова выводится, и так несколько раз. Пока ничего необычного, типичная ситуация -- скрипт делает какую-то работу с объектом и выводит в консоли то, как она продвигается. - -Необычное -- в другом! - -При раскрытии каждый объект будет выглядеть примерно так (скриншот из Chrome): - - - -**Судя по выводу, свойство `microsecond` всегда было равно `123459`... Или нет?** - -Если посмотреть на код выше то, очевидно, нет! Это свойство меняется, а консоль нас просто дурит. - -**При "раскрытии" свойств объекта в консоли -- браузер всегда выводит их текущие (на момент раскрытия) значения.** - -Так происходит именно потому, что вывод не делает "копию" текущего содержимого, а сохраняет лишь ссылку на объект. Запомните эту особенность консоли, в будущем, при отладке скриптов у вас не раз возникнет подобная ситуация. - - -## Итого - -
        -
      • Объект присваивается и копируется "по ссылке". То есть, в переменной хранится не сам объект а, условно говоря, адрес в памяти, где он находится.
      • -
      • Если переменная-объект скопирована или передана в функцию, то копируется именно эта ссылка, а объект остаётся один в памяти.
      • -
      - -Это -- одно из ключевых отличий объекта от примитива (числа, строки...), который при присвоении как раз копируется "по значению", то есть полностью. - diff --git a/1-js/4-data-structures/6-object-reference/object-reference-console.png b/1-js/4-data-structures/6-object-reference/object-reference-console.png deleted file mode 100644 index 9944a2df..00000000 Binary files a/1-js/4-data-structures/6-object-reference/object-reference-console.png and /dev/null differ diff --git a/1-js/4-data-structures/6-object-reference/object-reference-console@2x.png b/1-js/4-data-structures/6-object-reference/object-reference-console@2x.png deleted file mode 100644 index bfb0a3cf..00000000 Binary files a/1-js/4-data-structures/6-object-reference/object-reference-console@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/6-object-reference/variable-contains-reference.png b/1-js/4-data-structures/6-object-reference/variable-contains-reference.png deleted file mode 100644 index 53d18fed..00000000 Binary files a/1-js/4-data-structures/6-object-reference/variable-contains-reference.png and /dev/null differ diff --git a/1-js/4-data-structures/6-object-reference/variable-contains-reference@2x.png b/1-js/4-data-structures/6-object-reference/variable-contains-reference@2x.png deleted file mode 100644 index 9a152357..00000000 Binary files a/1-js/4-data-structures/6-object-reference/variable-contains-reference@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/6-object-reference/variable-copy-reference.png b/1-js/4-data-structures/6-object-reference/variable-copy-reference.png deleted file mode 100644 index b6a502c3..00000000 Binary files a/1-js/4-data-structures/6-object-reference/variable-copy-reference.png and /dev/null differ diff --git a/1-js/4-data-structures/6-object-reference/variable-copy-reference@2x.png b/1-js/4-data-structures/6-object-reference/variable-copy-reference@2x.png deleted file mode 100644 index 8e27b051..00000000 Binary files a/1-js/4-data-structures/6-object-reference/variable-copy-reference@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/6-object-reference/variable-copy-value.png b/1-js/4-data-structures/6-object-reference/variable-copy-value.png deleted file mode 100644 index 33578c15..00000000 Binary files a/1-js/4-data-structures/6-object-reference/variable-copy-value.png and /dev/null differ diff --git a/1-js/4-data-structures/6-object-reference/variable-copy-value@2x.png b/1-js/4-data-structures/6-object-reference/variable-copy-value@2x.png deleted file mode 100644 index 588762f5..00000000 Binary files a/1-js/4-data-structures/6-object-reference/variable-copy-value@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/7-array/1-get-last-in-array/solution.md b/1-js/4-data-structures/7-array/1-get-last-in-array/solution.md deleted file mode 100644 index 729b5d48..00000000 --- a/1-js/4-data-structures/7-array/1-get-last-in-array/solution.md +++ /dev/null @@ -1,16 +0,0 @@ -Последний элемент имеет индекс на `1` меньший, чем длина массива. - -Например: - -```js -var fruits = ["Яблоко", "Груша", "Слива"]; -``` - -Длина массива этого массива `fruits.length` равна `3`. Здесь "Яблоко" имеет индекс `0`, "Груша" -- индекс `1`, "Слива" -- индекс `2`. - -То есть, для массива длины `goods`: - -```js -var lastItem = goods[goods.length - 1]; // получить последний элемент -``` - diff --git a/1-js/4-data-structures/7-array/1-get-last-in-array/task.md b/1-js/4-data-structures/7-array/1-get-last-in-array/task.md deleted file mode 100644 index b33ea3d9..00000000 --- a/1-js/4-data-structures/7-array/1-get-last-in-array/task.md +++ /dev/null @@ -1,9 +0,0 @@ -# Получить последний элемент массива - -[importance 5] - -Как получить последний элемент из произвольного массива? - -У нас есть массив `goods`. Сколько в нем элементов -- не знаем, но можем прочитать из `goods.length`. - -Напишите код для получения последнего элемента `goods`. \ No newline at end of file diff --git a/1-js/4-data-structures/7-array/10-maximal-subarray/_js.view/solution.js b/1-js/4-data-structures/7-array/10-maximal-subarray/_js.view/solution.js deleted file mode 100644 index 5835fb09..00000000 --- a/1-js/4-data-structures/7-array/10-maximal-subarray/_js.view/solution.js +++ /dev/null @@ -1,10 +0,0 @@ -function getMaxSubSum(arr) { - var maxSum = 0, - partialSum = 0; - for (var i = 0; i < arr.length; i++) { - partialSum += arr[i]; - maxSum = Math.max(maxSum, partialSum); - if (partialSum < 0) partialSum = 0; - } - return maxSum; -} \ No newline at end of file diff --git a/1-js/4-data-structures/7-array/10-maximal-subarray/_js.view/test.js b/1-js/4-data-structures/7-array/10-maximal-subarray/_js.view/test.js deleted file mode 100644 index 4fc8605e..00000000 --- a/1-js/4-data-structures/7-array/10-maximal-subarray/_js.view/test.js +++ /dev/null @@ -1,33 +0,0 @@ -describe("getMaxSubSum", function() { - it("максимальная подсумма [1, 2, 3] равна 6", function() { - assert.equal(getMaxSubSum([1, 2, 3]), 6); - }); - - it("максимальная подсумма [-1, 2, 3, -9] равна 5", function() { - assert.equal(getMaxSubSum([-1, 2, 3, -9]), 5); - }); - - it("максимальная подсумма [-1, 2, 3, -9, 11] равна 11", function() { - assert.equal(getMaxSubSum([-1, 2, 3, -9, 11]), 11); - }); - - it("максимальная подсумма [-2, -1, 1, 2] равна 3", function() { - assert.equal(getMaxSubSum([-2, -1, 1, 2]), 3); - }); - - it("максимальная подсумма [100, -9, 2, -3, 5] равна 100", function() { - assert.equal(getMaxSubSum([100, -9, 2, -3, 5]), 100); - }); - - it("максимальная подсумма [] равна 0", function() { - assert.equal(getMaxSubSum([]), 0); - }); - - it("максимальная подсумма [-1] равна 0", function() { - assert.equal(getMaxSubSum([-1]), 0); - }); - - it("максимальная подсумма [-1, -2] равна 0", function() { - assert.equal(getMaxSubSum([-1, -2]), 0); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/7-array/10-maximal-subarray/solution.md b/1-js/4-data-structures/7-array/10-maximal-subarray/solution.md deleted file mode 100644 index 355b5d0c..00000000 --- a/1-js/4-data-structures/7-array/10-maximal-subarray/solution.md +++ /dev/null @@ -1,101 +0,0 @@ -# Подсказка (медленное решение) -Можно просто посчитать для каждого элемента массива все суммы, которые с него начинаются. - -Например, для `[-1, 2, 3, -9, 11]`: - -```js -//+ no-beautify -// Начиная с -1: --1 --1 + 2 --1 + 2 + 3 --1 + 2 + 3 + (-9) --1 + 2 + 3 + (-9) + 11 - -// Начиная с 2: -2 -2 + 3 -2 + 3 + (-9) -2 + 3 + (-9) + 11 - -// Начиная с 3: -3 -3 + (-9) -3 + (-9) + 11 - -// Начиная с -9 --9 --9 + 11 - -// Начиная с -11 --11 -``` - -Сделайте вложенный цикл, который на внешнем уровне бегает по элементам массива, а на внутреннем -- формирует все суммы элементов, которые начинаются с текущей позиции. - -# Медленное решение - -Решение через вложенный цикл: - -```js -//+ run -function getMaxSubSum(arr) { - var maxSum = 0; // если совсем не брать элементов, то сумма 0 - - for (var i = 0; i < arr.length; i++) { - var sumFixedStart = 0; - for (var j = i; j < arr.length; j++) { - sumFixedStart += arr[j]; - maxSum = Math.max(maxSum, sumFixedStart); - } - } - - return maxSum; -} - -alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 -alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 -alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 -alert( getMaxSubSum([1, 2, 3]) ); // 6 -alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 -``` - -Такое решение имеет [оценку сложности](http://ru.wikipedia.org/wiki/%C2%ABO%C2%BB_%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%BE%D0%B5_%D0%B8_%C2%ABo%C2%BB_%D0%BC%D0%B0%D0%BB%D0%BE%D0%B5) O(n2), то есть при увеличении массива в 2 раза алгоритм требует в 4 раза больше времени. На больших массивах (1000, 10000 и более элементов) такие алгоритмы могут приводить к серьёзным "тормозам". - -# Подсказка (быстрое решение) - -Будем идти по массиву и накапливать в некоторой переменной `s` текущую частичную сумму. Если в какой-то момент s окажется отрицательной, то мы просто присвоим `s=0`. Утверждается, что максимум из всех значений переменной s, случившихся за время работы, и будет ответом на задачу. - -**Докажем этот алгоритм.** - -В самом деле, рассмотрим первый момент времени, когда сумма `s` стала отрицательной. Это означает, что, стартовав с нулевой частичной суммы, мы в итоге пришли к отрицательной частичной сумме -- значит, и весь этот префикс массива, равно как и любой его суффикс имеют отрицательную сумму. - -Следовательно, от всего этого префикса массива в дальнейшем не может быть никакой пользы: он может дать только отрицательную прибавку к ответу. - -# Быстрое решение - -```js -//+ run -function getMaxSubSum(arr) { - var maxSum = 0, - partialSum = 0; - for (var i = 0; i < arr.length; i++) { - partialSum += arr[i]; - maxSum = Math.max(maxSum, partialSum); - if (partialSum < 0) partialSum = 0; - } - return maxSum; -} - - -alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 -alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 -alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 -alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 -alert( getMaxSubSum([1, 2, 3]) ); // 6 -alert( getMaxSubSum([-1, -2, -3]) ); // 0 -``` - -Информацию об алгоритме вы также можете прочитать здесь: [](http://e-maxx.ru/algo/maximum_average_segment) и здесь: [Maximum subarray problem](http://en.wikipedia.org/wiki/Maximum_subarray_problem). - -Этот алгоритм требует ровно одного прохода по массиву, его сложность имеет оценку `O(n)`. \ No newline at end of file diff --git a/1-js/4-data-structures/7-array/10-maximal-subarray/task.md b/1-js/4-data-structures/7-array/10-maximal-subarray/task.md deleted file mode 100644 index 2381c0f6..00000000 --- a/1-js/4-data-structures/7-array/10-maximal-subarray/task.md +++ /dev/null @@ -1,28 +0,0 @@ -# Подмассив наибольшей суммы - -[importance 2] - -На входе массив чисел, например: `arr = [1, -2, 3, 4, -9, 6]`. - -Задача -- найти непрерывный подмассив `arr`, сумма элементов которого максимальна. - -Ваша функция должна возвращать только эту сумму. - -Например: - -```js -getMaxSubSum([-1, *!*2, 3*/!*, -9]) = 5 (сумма выделенных) -getMaxSubSum([*!*2, -1, 2, 3*/!*, -9]) = 6 -getMaxSubSum([-1, 2, 3, -9, *!*11*/!*]) = 11 -getMaxSubSum([-2, -1, *!*1, 2*/!*]) = 3 -getMaxSubSum([*!*100*/!*, -9, 2, -3, 5]) = 100 -getMaxSubSum([*!*1, 2, 3*/!*]) = 6 (неотрицательные - берем всех) -``` - -Если все элементы отрицательные, то не берём ни одного элемента и считаем сумму равной нулю: - -```js -getMaxSubSum([-1, -2, -3]) = 0 -``` - -Постарайтесь придумать решение, которое работает за O(n2), а лучше за O(n) операций. \ No newline at end of file diff --git a/1-js/4-data-structures/7-array/2-add-item-to-array/solution.md b/1-js/4-data-structures/7-array/2-add-item-to-array/solution.md deleted file mode 100644 index 9f72e68b..00000000 --- a/1-js/4-data-structures/7-array/2-add-item-to-array/solution.md +++ /dev/null @@ -1,6 +0,0 @@ -Текущий последний элемент имеет индекс `goods.length-1`. Значит, индексом нового элемента будет `goods.length`: - -```js -goods[goods.length] = 'Компьютер' -``` - diff --git a/1-js/4-data-structures/7-array/2-add-item-to-array/task.md b/1-js/4-data-structures/7-array/2-add-item-to-array/task.md deleted file mode 100644 index 6f0e6d16..00000000 --- a/1-js/4-data-structures/7-array/2-add-item-to-array/task.md +++ /dev/null @@ -1,7 +0,0 @@ -# Добавить новый элемент в массив - -[importance 5] - -Как добавить элемент в конец произвольного массива? - -У нас есть массив `goods`. Напишите код для добавления в его конец значения "Компьютер". diff --git a/1-js/4-data-structures/7-array/3-create-array/solution.md b/1-js/4-data-structures/7-array/3-create-array/solution.md deleted file mode 100644 index 7b518dbf..00000000 --- a/1-js/4-data-structures/7-array/3-create-array/solution.md +++ /dev/null @@ -1,11 +0,0 @@ - - -```js -//+ run -var styles = ["Джаз", "Блюз"]; -styles.push("Рок-н-Ролл"); -styles[styles.length - 2] = "Классика"; -alert( styles.shift() ); -styles.unshift("Рэп", "Регги "); -``` - diff --git a/1-js/4-data-structures/7-array/3-create-array/task.md b/1-js/4-data-structures/7-array/3-create-array/task.md deleted file mode 100644 index 0426c89c..00000000 --- a/1-js/4-data-structures/7-array/3-create-array/task.md +++ /dev/null @@ -1,24 +0,0 @@ -# Создание массива - -[importance 5] - -Задача из 5 шагов-строк: -
        -
      1. Создайте массив `styles` с элементами "Джаз", "Блюз".
      2. -
      3. Добавьте в конец значение "Рок-н-Ролл"
      4. -
      5. Замените предпоследнее значение с конца на "Классика". Код замены предпоследнего значения должен работать для массивов любой длины.
      6. -
      7. Удалите первое значение массива и выведите его `alert`.
      8. -
      9. Добавьте в начало значения "Рэп" и "Регги".
      10. -
      - -Массив в результате каждого шага: - -```js -//+ no-beautify -Джаз, Блюз -Джаз, Блюз, Рок-н-Ролл -Джаз, Классика, Рок-н-Ролл -Классика, Рок-н-Ролл -Рэп, Регги, Классика, Рок-н-Ролл -``` - diff --git a/1-js/4-data-structures/7-array/4-random-from-array/solution.md b/1-js/4-data-structures/7-array/4-random-from-array/solution.md deleted file mode 100644 index a121e5ab..00000000 --- a/1-js/4-data-structures/7-array/4-random-from-array/solution.md +++ /dev/null @@ -1,11 +0,0 @@ -Для вывода нужен случайный номер от `0` до `arr.length-1` включительно. - -```js -//+ run -var arr = ["Яблоко", "Апельсин", "Груша", "Лимон"]; - -var rand = Math.floor(Math.random() * arr.length); - -alert( arr[rand] ); -``` - diff --git a/1-js/4-data-structures/7-array/4-random-from-array/task.md b/1-js/4-data-structures/7-array/4-random-from-array/task.md deleted file mode 100644 index 6a74b033..00000000 --- a/1-js/4-data-structures/7-array/4-random-from-array/task.md +++ /dev/null @@ -1,16 +0,0 @@ -# Получить случайное значение из массива - -[importance 3] - -Напишите код для вывода `alert` случайного значения из массива: - -```js -var arr = ["Яблоко", "Апельсин", "Груша", "Лимон"]; -``` - -P.S. Код для генерации случайного целого от `min` to `max` включительно: - -```js -var rand = min + Math.floor(Math.random() * (max + 1 - min)); -``` - diff --git a/1-js/4-data-structures/7-array/5-calculator-for-input/solution.md b/1-js/4-data-structures/7-array/5-calculator-for-input/solution.md deleted file mode 100644 index 70176c1d..00000000 --- a/1-js/4-data-structures/7-array/5-calculator-for-input/solution.md +++ /dev/null @@ -1,23 +0,0 @@ -В решение ниже обратите внимание: мы не приводим `value` к числу сразу после `prompt`, так как если сделать `value = +value`, то после этого отличить пустую строку от нуля уже никак нельзя. А нам здесь нужно при пустой строке прекращать ввод, а при нуле -- продолжать. - -```js -//+ run demo -var numbers = []; - -while (true) { - - var value = prompt("Введите число", 0); - - if (value === "" || value === null || isNaN(value)) break; - - numbers.push(+value); -} - -var sum = 0; -for (var i = 0; i < numbers.length; i++) { - sum += numbers[i]; -} - -alert( sum ); -``` - diff --git a/1-js/4-data-structures/7-array/5-calculator-for-input/task.md b/1-js/4-data-structures/7-array/5-calculator-for-input/task.md deleted file mode 100644 index 6c3cfab5..00000000 --- a/1-js/4-data-structures/7-array/5-calculator-for-input/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Создайте калькулятор для введённых значений - -[importance 4] - -Напишите код, который: -
        -
      • Запрашивает по очереди значения при помощи `prompt` и сохраняет их в массиве.
      • -
      • Заканчивает ввод, как только посетитель введёт пустую строку, не число или нажмёт "Отмена".
      • -
      • При этом ноль `0` не должен заканчивать ввод, это разрешённое число.
      • -
      • Выводит сумму всех значений массива
      • -
      - -[demo /] \ No newline at end of file diff --git a/1-js/4-data-structures/7-array/6-item-value/solution.md b/1-js/4-data-structures/7-array/6-item-value/solution.md deleted file mode 100644 index 8d3c2ac1..00000000 --- a/1-js/4-data-structures/7-array/6-item-value/solution.md +++ /dev/null @@ -1,24 +0,0 @@ - - -```js -//+ run -var arr = [1, 2, 3]; - -var arr2 = arr; // (*) -arr2[0] = 5; - -alert( arr[0] ); -alert( arr2[0] ); -``` - -Код выведет `5` в обоих случаях, так как массив является объектом. В строке `(*)` в переменную `arr2` копируется ссылка на него, а сам объект в памяти по-прежнему один, в нём отражаются изменения, внесенные через `arr2` или `arr`. - -В частности, сравнение `arr2 == arr` даст `true`. - -Если нужно именно скопировать массив, то это можно сделать, например, так: - -```js -var arr2 = []; -for (var i = 0; i < arr.length; i++) arr2[i] = arr[i]; -``` - diff --git a/1-js/4-data-structures/7-array/6-item-value/task.md b/1-js/4-data-structures/7-array/6-item-value/task.md deleted file mode 100644 index e9a080b9..00000000 --- a/1-js/4-data-structures/7-array/6-item-value/task.md +++ /dev/null @@ -1,18 +0,0 @@ -# Чему равен элемент массива? - -[importance 3] - -Что выведет этот код? - -```js -var arr = [1, 2, 3]; - -var arr2 = arr; -arr2[0] = 5; - -*!* -alert( arr[0] ); -alert( arr2[0] ); -*/!* -``` - diff --git a/1-js/4-data-structures/7-array/7-array-find/_js.view/solution.js b/1-js/4-data-structures/7-array/7-array-find/_js.view/solution.js deleted file mode 100644 index c172ef0a..00000000 --- a/1-js/4-data-structures/7-array/7-array-find/_js.view/solution.js +++ /dev/null @@ -1,11 +0,0 @@ -function find(array, value) { - if (array.indexOf) { // если метод существует - return array.indexOf(value); - } - - for (var i = 0; i < array.length; i++) { - if (array[i] === value) return i; - } - - return -1; -} \ No newline at end of file diff --git a/1-js/4-data-structures/7-array/7-array-find/_js.view/test.js b/1-js/4-data-structures/7-array/7-array-find/_js.view/test.js deleted file mode 100644 index 86b958ba..00000000 --- a/1-js/4-data-structures/7-array/7-array-find/_js.view/test.js +++ /dev/null @@ -1,26 +0,0 @@ -describe("find", function() { - - describe("возвращает позицию, на которой найден элемент", function() { - it("в массиве [1,2,3] находит 1 на позиции 0", function() { - assert.equal(find([1, 2, 3], 1), 0); - }); - it("в массиве [1,2,3] находит 2 на позиции 1", function() { - assert.equal(find([1, 2, 3], 2), 1); - }); - it("в массиве [1,2,3] находит 3 на позиции 2", function() { - assert.equal(find([1, 2, 3], 3), 2); - }); - }); - - it("если элемент не найден, возвращает -1", function() { - assert.equal(find([1, 2, 3], 0), -1); - }); - - it("отличает false или null от 0", function() { - assert.equal(find([false, true, null], 0), -1); - }); - - it("отличает 1 от true", function() { - assert.equal(find([1, 2, 3], true), -1); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/7-array/7-array-find/solution.md b/1-js/4-data-structures/7-array/7-array-find/solution.md deleted file mode 100644 index 3c83019b..00000000 --- a/1-js/4-data-structures/7-array/7-array-find/solution.md +++ /dev/null @@ -1,61 +0,0 @@ -Возможное решение: - -```js -function find(array, value) { - - for (var i = 0; i < array.length; i++) { - if (array[i] == value) return i; - } - - return -1; -} -``` - -Однако, в нем ошибка, т.к. сравнение `==` не различает `0` и `false`. - -Поэтому лучше использовать `===`. Кроме того, в современном стандарте JavaScript существует встроенная функция Array#indexOf, которая работает именно таким образом. Имеет смысл ей воспользоваться, если браузер ее поддерживает. - -```js -//+ run -function find(array, value) { - if (array.indexOf) { // если метод существует - return array.indexOf(value); - } - - for (var i = 0; i < array.length; i++) { - if (array[i] === value) return i; - } - - return -1; -} - -var arr = ["a", -1, 2, "b"]; - -var index = find(arr, 2); - -alert( index ); -``` - -... Но еще лучшим вариантом было бы определить `find` по-разному в зависимости от поддержки браузером метода `indexOf`: - -```js -// создаем пустой массив и проверяем поддерживается ли indexOf -if ([].indexOf) { - - var find = function(array, value) { - return array.indexOf(value); - } - -} else { - var find = function(array, value) { - for (var i = 0; i < array.length; i++) { - if (array[i] === value) return i; - } - - return -1; - } - -} -``` - -Этот способ - лучше всего, т.к. не требует при каждом запуске `find` проверять поддержку `indexOf`. diff --git a/1-js/4-data-structures/7-array/7-array-find/task.md b/1-js/4-data-structures/7-array/7-array-find/task.md deleted file mode 100644 index f32038e4..00000000 --- a/1-js/4-data-structures/7-array/7-array-find/task.md +++ /dev/null @@ -1,18 +0,0 @@ -# Поиск в массиве - -[importance 3] - -Создайте функцию `find(arr, value)`, которая ищет в массиве `arr` значение `value` и возвращает его номер, если найдено, или `-1`, если не найдено. - -Например: - -```js -arr = ["test", 2, 1.5, false]; - -find(arr, "test"); // 0 -find(arr, 2); // 1 -find(arr, 1.5); // 2 - -find(arr, 0); // -1 -``` - diff --git a/1-js/4-data-structures/7-array/8-filter-range/_js.view/solution.js b/1-js/4-data-structures/7-array/8-filter-range/_js.view/solution.js deleted file mode 100644 index 58002f54..00000000 --- a/1-js/4-data-structures/7-array/8-filter-range/_js.view/solution.js +++ /dev/null @@ -1,11 +0,0 @@ -function filterRange(arr, a, b) { - var result = []; - - for (var i = 0; i < arr.length; i++) { - if (arr[i] >= a && arr[i] <= b) { - result.push(arr[i]); - } - } - - return result; -} \ No newline at end of file diff --git a/1-js/4-data-structures/7-array/8-filter-range/_js.view/test.js b/1-js/4-data-structures/7-array/8-filter-range/_js.view/test.js deleted file mode 100644 index 4cb4cbf6..00000000 --- a/1-js/4-data-structures/7-array/8-filter-range/_js.view/test.js +++ /dev/null @@ -1,15 +0,0 @@ -describe("filterRange", function() { - it("не меняет исходный массив", function() { - var arr = [5, 4, 3, 8, 0]; - - filterRange(arr, 0, 10); - assert.deepEqual(arr, [5, 4, 3, 8, 0]); - }); - - it("оставляет только значения указанного интервала", function() { - var arr = [5, 4, 3, 8, 0]; - - var result = filterRange(arr, 3, 5); - assert.deepEqual(result, [5, 4, 3]); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/7-array/8-filter-range/solution.md b/1-js/4-data-structures/7-array/8-filter-range/solution.md deleted file mode 100644 index 37421191..00000000 --- a/1-js/4-data-structures/7-array/8-filter-range/solution.md +++ /dev/null @@ -1,29 +0,0 @@ -# Алгоритм решения -
        -
      1. Создайте временный пустой массив `var results = []`.
      2. -
      3. Пройдите по элементам `arr` в цикле и заполните его.
      4. -
      5. Возвратите `results`.
      6. -
      - -# Решение - -```js -//+ run -function filterRange(arr, a, b) { - var result = []; - - for (var i = 0; i < arr.length; i++) { - if (arr[i] >= a && arr[i] <= b) { - result.push(arr[i]); - } - } - - return result; -} - -var arr = [5, 4, 3, 8, 0]; - -var filtered = filterRange(arr, 3, 5); -alert( filtered ); -``` - diff --git a/1-js/4-data-structures/7-array/8-filter-range/task.md b/1-js/4-data-structures/7-array/8-filter-range/task.md deleted file mode 100644 index 428d1a60..00000000 --- a/1-js/4-data-structures/7-array/8-filter-range/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# Фильтр диапазона - -[importance 3] - -Создайте функцию `filterRange(arr, a, b)`, которая принимает массив чисел `arr` и возвращает новый массив, который содержит только числа из `arr` из диапазона от `a` до `b`. То есть, проверка имеет вид `a ≤ arr[i] ≤ b`. -Функция не должна менять `arr`. - -Пример работы: - -```js -var arr = [5, 4, 3, 8, 0]; - -var filtered = filterRange(arr, 3, 5); -// теперь filtered = [5, 4, 3] -// arr не изменился -``` - diff --git a/1-js/4-data-structures/7-array/9-eratosthenes-sieve/sieve.gif b/1-js/4-data-structures/7-array/9-eratosthenes-sieve/sieve.gif deleted file mode 100644 index d1960cef..00000000 Binary files a/1-js/4-data-structures/7-array/9-eratosthenes-sieve/sieve.gif and /dev/null differ diff --git a/1-js/4-data-structures/7-array/9-eratosthenes-sieve/solution.md b/1-js/4-data-structures/7-array/9-eratosthenes-sieve/solution.md deleted file mode 100644 index 8d334a36..00000000 --- a/1-js/4-data-structures/7-array/9-eratosthenes-sieve/solution.md +++ /dev/null @@ -1,40 +0,0 @@ -Их сумма равна `1060`. - -```js -//+ run -// шаг 1 -var arr = []; - -for (var i = 2; i < 100; i++) { - arr[i] = true -} - -// шаг 2 -var p = 2; - -do { - // шаг 3 - for (i = 2 * p; i < 100; i += p) { - arr[i] = false; - } - - // шаг 4 - for (i = p + 1; i < 100; i++) { - if (arr[i]) break; - } - - p = i; -} while (p * p < 100); // шаг 5 - -// шаг 6 (готово) -// посчитать сумму -var sum = 0; -for (i = 0; i < arr.length; i++) { - if (arr[i]) { - sum += i; - } -} - -alert( sum ); -``` - diff --git a/1-js/4-data-structures/7-array/9-eratosthenes-sieve/task.md b/1-js/4-data-structures/7-array/9-eratosthenes-sieve/task.md deleted file mode 100644 index fd68c82b..00000000 --- a/1-js/4-data-structures/7-array/9-eratosthenes-sieve/task.md +++ /dev/null @@ -1,25 +0,0 @@ -# Решето Эратосфена - -[importance 3] - -Целое число, большее `1`, называется *простым*, если оно не делится нацело ни на какое другое, кроме себя и `1`. - -Древний алгоритм "Решето Эратосфена" для поиска всех простых чисел до `n` выглядит так: - -
        -
      1. Создать список последовательных чисел от `2` до `n`: `2, 3, 4, ..., n`.
      2. -
      3. Пусть `p=2`, это первое простое число.
      4. -
      5. Зачеркнуть все последующие числа в списке с разницей в `p`, т.е. `2*p, 3*p, 4*p` и т.д. В случае `p=2` это будут `4,6,8...`.
      6. -
      7. Поменять значение `p` на первое не зачеркнутое число после `p`.
      8. -
      9. Повторить шаги 3-4 пока p2 < n.
      10. -
      11. Все оставшиеся не зачеркнутыми числа -- простые.
      12. -
      - -Посмотрите также [анимацию алгоритма](sieve.gif). - -Реализуйте "Решето Эратосфена" в JavaScript, используя массив. - -Найдите все простые числа до `100` и выведите их сумму. - - - diff --git a/1-js/4-data-structures/7-array/array-pop.png b/1-js/4-data-structures/7-array/array-pop.png deleted file mode 100644 index 6583cbe7..00000000 Binary files a/1-js/4-data-structures/7-array/array-pop.png and /dev/null differ diff --git a/1-js/4-data-structures/7-array/array-pop@2x.png b/1-js/4-data-structures/7-array/array-pop@2x.png deleted file mode 100644 index 00c3a1fa..00000000 Binary files a/1-js/4-data-structures/7-array/array-pop@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/7-array/array-shift.png b/1-js/4-data-structures/7-array/array-shift.png deleted file mode 100644 index a0234446..00000000 Binary files a/1-js/4-data-structures/7-array/array-shift.png and /dev/null differ diff --git a/1-js/4-data-structures/7-array/array-shift@2x.png b/1-js/4-data-structures/7-array/array-shift@2x.png deleted file mode 100644 index 8798c0dc..00000000 Binary files a/1-js/4-data-structures/7-array/array-shift@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/7-array/array-speed.png b/1-js/4-data-structures/7-array/array-speed.png deleted file mode 100644 index 521b25bb..00000000 Binary files a/1-js/4-data-structures/7-array/array-speed.png and /dev/null differ diff --git a/1-js/4-data-structures/7-array/array-speed@2x.png b/1-js/4-data-structures/7-array/array-speed@2x.png deleted file mode 100644 index 05f5c483..00000000 Binary files a/1-js/4-data-structures/7-array/array-speed@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/7-array/article.md b/1-js/4-data-structures/7-array/article.md deleted file mode 100644 index b7d4063a..00000000 --- a/1-js/4-data-structures/7-array/article.md +++ /dev/null @@ -1,462 +0,0 @@ -# Массивы c числовыми индексами - -*Массив* -- разновидность объекта, которая предназначена для хранения пронумерованных значений и предлагает дополнительные методы для удобного манипулирования такой коллекцией. - -Они обычно используются для хранения упорядоченных коллекций данных, например -- списка товаров на странице, студентов в группе и т.п. - -[cut] - -## Объявление - -Синтаксис для создания нового массива -- квадратные скобки со списком элементов внутри. - -Пустой массив: - -```js -var arr = []; -``` - -Массив `fruits` с тремя элементами: - -```js -var fruits = ["Яблоко", "Апельсин", "Слива"]; -``` - -**Элементы нумеруются, начиная с нуля.** - -Чтобы получить нужный элемент из массива -- указывается его номер в квадратных скобках: - -```js -//+ run -var fruits = ["Яблоко", "Апельсин", "Слива"]; - -alert( fruits[0] ); // Яблоко -alert( fruits[1] ); // Апельсин -alert( fruits[2] ); // Слива -``` - -Элемент можно всегда заменить: - -```js -fruits[2] = 'Груша'; // теперь ["Яблоко", "Апельсин", "Груша"] -``` - -...Или добавить: - -```js -fruits[3] = 'Лимон'; // теперь ["Яблоко", "Апельсин", "Груша", "Лимон"] -``` - -Общее число элементов, хранимых в массиве, содержится в его свойстве `length`: - -```js -//+ run -var fruits = ["Яблоко", "Апельсин", "Груша"]; - -alert( fruits.length ); // 3 -``` - -**Через `alert` можно вывести и массив целиком.** - -При этом его элементы будут перечислены через запятую: - -```js -//+ run -var fruits = ["Яблоко", "Апельсин", "Груша"]; - -alert( fruits ); // Яблоко,Апельсин,Груша -``` - -**В массиве может храниться любое число элементов любого типа.** - -В том числе, строки, числа, объекты, вот например: - -```js -//+ run no-beautify -// микс значений -var arr = [ 1, 'Имя', { name: 'Петя' }, true ]; - -// получить объект из массива и тут же -- его свойство -alert( arr[2].name ); // Петя -``` - -## Методы pop/push, shift/unshift - -Одно из применений массива -- это [очередь](http://ru.wikipedia.org/wiki/%D0%9E%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C_%28%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%29). В классическом программировании так называют упорядоченную коллекцию элементов, такую что элементы добавляются в конец, а обрабатываются -- с начала. - - - -В реальной жизни эта структура данных встречается очень часто. Например, очередь сообщений, которые надо показать на экране. - -Очень близка к очереди еще одна структура данных: [стек](http://ru.wikipedia.org/wiki/%D0%A1%D1%82%D0%B5%D0%BA). Это такая коллекция элементов, в которой новые элементы добавляются в конец и берутся с конца. - - - -Например, стеком является колода карт, в которую новые карты кладутся сверху, и берутся -- тоже сверху. - -Для того, чтобы реализовывать эти структуры данных, и просто для более удобной работы с началом и концом массива существуют специальные методы. - -### Конец массива - -
      -
      `pop`
      -
      Удаляет *последний* элемент из массива и возвращает его: - -```js -//+ run -var fruits = ["Яблоко", "Апельсин", "Груша"]; - -alert( fruits.pop() ); // удалили "Груша" - -alert( fruits ); // Яблоко, Апельсин -``` - -
      -
      `push`
      -
      Добавляет элемент *в конец* массива: - -```js -//+ run -var fruits = ["Яблоко", "Апельсин"]; - -fruits.push("Груша"); - -alert( fruits ); // Яблоко, Апельсин, Груша -``` - -Вызов `fruits.push(...)` равнозначен `fruits[fruits.length] = ...`. -
      -
      - -### Начало массива - -
      -
      `shift`
      -
      Удаляет из массива *первый* элемент и возвращает его: - -```js -var fruits = ["Яблоко", "Апельсин", "Груша"]; - -alert( fruits.shift() ); // удалили Яблоко - -alert( fruits ); // Апельсин, Груша -``` - -
      -
      `unshift`
      -
      Добавляет элемент *в начало* массива: - -```js -var fruits = ["Апельсин", "Груша"]; - -fruits.unshift('Яблоко'); - -alert( fruits ); // Яблоко, Апельсин, Груша -``` - -
      -
      - -Методы `push` и `unshift` могут добавлять сразу по несколько элементов: - -```js -//+ run -var fruits = ["Яблоко"]; - -fruits.push("Апельсин", "Персик"); -fruits.unshift("Ананас", "Лимон"); - -// результат: ["Ананас", "Лимон", "Яблоко", "Апельсин", "Персик"] -alert( fruits ); -``` - -## Внутреннее устройство массива - -Массив -- это объект, где в качестве ключей выбраны цифры, с дополнительными методами и свойством `length`. - -Так как это объект, то в функцию он передаётся по ссылке: - -```js -//+ run -function eat(arr) { - arr.pop(); -} - -var arr = ["нам", "не", "страшен", "серый", "волк"] - -alert( arr.length ); // 5 -eat(arr); -eat(arr); -alert( arr.length ); // 3, в функцию массив не скопирован, а передана ссылка -``` - -**Ещё одно следствие -- можно присваивать в массив любые свойства.** - -Например: - -```js -var fruits = []; // создать массив - -fruits[99999] = 5; // присвоить свойство с любым номером - -fruits.age = 25; // назначить свойство со строковым именем -``` - -.. Но массивы для того и придуманы в JavaScript, чтобы удобно работать именно *с упорядоченными, нумерованными данными*. Для этого в них существуют специальные методы и свойство `length`. - -Как правило, нет причин использовать массив как обычный объект, хотя технически это и возможно. - -[warn header="Вывод массива с \"дырами\""] -Если в массиве есть пропущенные индексы, то при выводе в большинстве браузеров появляются "лишние" запятые, например: - -```js -//+ run -var a = []; -a[0] = 0; -a[5] = 5; - -alert( a ); // 0,,,,,5 -``` - -Эти запятые появляются потому, что алгоритм вывода массива идёт от `0` до `arr.length` и выводит всё через запятую. Отсутствие значений даёт несколько запятых подряд. -[/warn] - -### Влияние на быстродействие - -Методы `push/pop` выполняются быстро, а `shift/unshift` -- медленно. - - - - -Чтобы понять, почему работать с концом массива -- быстрее, чем с его началом, разберём подробнее происходящее при операции: - -```js -fruits.shift(); // убрать 1 элемент с начала -``` - -При этом, так как все элементы находятся в своих ячейках, просто удалить элемент с номером `0` недостаточно. Нужно еще и переместить остальные элементы на их новые индексы. - -Операция `shift` должна выполнить целых три действия: -
        -
      1. Удалить нулевой элемент.
      2. -
      3. Переместить все свойства влево, с индекса `1` на `0`, с `2` на `1` и так далее.
      4. -
      5. Обновить свойство `length`.
      6. -
      - - - -**Чем больше элементов в массиве, тем дольше их перемещать, это много операций с памятью.** - -Аналогично работает `unshift`: чтобы добавить элемент в начало массива, нужно сначала перенести вправо, в увеличенные индексы, все существующие. - -А что же с `push/pop`? Им как раз перемещать ничего не надо. Для того, чтобы удалить элемент, метод `pop` очищает ячейку и укорачивает `length`. - -Действия при операции: - -```js -fruits.pop(); // убрать 1 элемент с конца -``` - - - -**Перемещать при `pop` не требуется, так как прочие элементы после этой операции остаются на тех же индексах.** - -Аналогично работает `push`. - - -## Перебор элементов - -Для перебора элементов обычно используется цикл: - -```js -//+ run -var arr = ["Яблоко", "Апельсин", "Груша"]; - -*!* -for (var i = 0; i < arr.length; i++) { - alert( arr[i] ); -} -*/!* -``` - -[warn header="Не используйте `for..in` для массивов"] -Так как массив является объектом, то возможен и вариант `for..in`: - -```js -//+ run -var arr = ["Яблоко", "Апельсин", "Груша"]; - -*!* -for (var key in arr) { -*/!* - alert( arr[key] ); // Яблоко, Апельсин, Груша -} -``` - -Недостатки этого способа: - -
        -
      1. Цикл `for..in` выведет *все свойства* объекта, а не только цифровые. - -В браузере, при работе с объектами страницы, встречаются коллекции элементов, которые по виду как массивы, но имеют дополнительные нецифровые свойства. При переборе таких "похожих на массив" коллекций через `for..in` эти свойства будут выведены, а они как раз не нужны. - -Бывают и библиотеки, которые предоставляют такие коллекции. Классический `for` надёжно выведет только цифровые свойства, что обычно и требуется. -
      2. -
      3. Цикл `for (var i=0; i -
      - -Если коротко: цикл `for(var i=0; i -
    4. Не ставить массиву произвольные свойства, такие как `arr.test = 5`. То есть, работать именно как с массивом, а не как с объектом.
    5. -
    6. Заполнять массив непрерывно и по возрастающей. Как только браузер встречает необычное поведение массива, например устанавливается значение `arr[0]`, а потом сразу `arr[1000]`, то он начинает работать с ним, как с обычным объектом. Как правило, это влечёт преобразование его в хэш-таблицу.
    7. - - - -Если следовать этим принципам, то массивы будут занимать меньше памяти и быстрее работать. - -## Итого - -Массивы существуют для работы с упорядоченным набором элементов. - -**Объявление:** - -```js -// предпочтительное -var arr = [элемент1, элемент2...]; - -// new Array -var arr = new Array(элемент1, элемент2...); -``` - -При этом `new Array(число)` создаёт массив заданной длины, *без элементов*. Чтобы избежать ошибок, предпочтителен первый синтаксис. - -**Свойство `length`** -- длина массива. Если точнее, то последний индекс массива плюс `1`. Если её уменьшить вручную, то массив укоротится. Если `length` больше реального количества элементов, то отсутствующие элементы равны `undefined`. - - -Массив можно использовать как очередь или стек. - -**Операции с концом массива:** -
        -
      • `arr.push(элемент1, элемент2...)` добавляет элементы в конец.
      • -
      • `var elem = arr.pop()` удаляет и возвращает последний элемент.
      • -
      - -**Операции с началом массива:** -
        -
      • `arr.unshift(элемент1, элемент2...)` добавляет элементы в начало.
      • -
      • `var elem = arr.shift()` удаляет и возвращает первый элемент.
      • -
      - -Эти операции перенумеровывают все элементы, поэтому работают медленно. - -В следующей главе мы рассмотрим другие методы для работы с массивами. - - - - diff --git a/1-js/4-data-structures/7-array/queue.png b/1-js/4-data-structures/7-array/queue.png deleted file mode 100644 index 8a90ef30..00000000 Binary files a/1-js/4-data-structures/7-array/queue.png and /dev/null differ diff --git a/1-js/4-data-structures/7-array/queue@2x.png b/1-js/4-data-structures/7-array/queue@2x.png deleted file mode 100644 index 2a1281d0..00000000 Binary files a/1-js/4-data-structures/7-array/queue@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/7-array/stack.png b/1-js/4-data-structures/7-array/stack.png deleted file mode 100644 index 566da719..00000000 Binary files a/1-js/4-data-structures/7-array/stack.png and /dev/null differ diff --git a/1-js/4-data-structures/7-array/stack@2x.png b/1-js/4-data-structures/7-array/stack@2x.png deleted file mode 100644 index 9301457d..00000000 Binary files a/1-js/4-data-structures/7-array/stack@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/8-array-methods/1-add-class/_js.view/solution.js b/1-js/4-data-structures/8-array-methods/1-add-class/_js.view/solution.js deleted file mode 100644 index ee254d29..00000000 --- a/1-js/4-data-structures/8-array-methods/1-add-class/_js.view/solution.js +++ /dev/null @@ -1,11 +0,0 @@ -function addClass(obj, cls) { - var classes = obj.className ? obj.className.split(' ') : []; - - for (var i = 0; i < classes.length; i++) { - if (classes[i] == cls) return; // класс уже есть - } - - classes.push(cls); // добавить - - obj.className = classes.join(' '); // и обновить свойство -} \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/1-add-class/_js.view/test.js b/1-js/4-data-structures/8-array-methods/1-add-class/_js.view/test.js deleted file mode 100644 index 9ad36939..00000000 --- a/1-js/4-data-structures/8-array-methods/1-add-class/_js.view/test.js +++ /dev/null @@ -1,33 +0,0 @@ -describe("addClass", function() { - - it("добавляет класс, которого нет", function() { - var obj = { - className: 'open menu' - }; - addClass(obj, 'new'); - assert.deepEqual(obj, { - className: 'open menu new' - }); - }); - - it("не добавляет класс, который уже есть", function() { - var obj = { - className: 'open menu' - }; - addClass(obj, 'open'); - assert.deepEqual(obj, { - className: 'open menu' - }); - }); - - it("не добавляет лишних пробелов, который уже есть", function() { - var obj = { - className: '' - }; - addClass(obj, 'open'); - assert.deepEqual(obj, { - className: 'open' - }); - }); - -}); \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/1-add-class/solution.md b/1-js/4-data-structures/8-array-methods/1-add-class/solution.md deleted file mode 100644 index 4cea0432..00000000 --- a/1-js/4-data-structures/8-array-methods/1-add-class/solution.md +++ /dev/null @@ -1,30 +0,0 @@ -Решение заключается в превращении `obj.className` в массив при помощи `split`. -После этого в нем можно проверить наличие класса, и если нет - добавить. - -```js -//+ run -function addClass(obj, cls) { - var classes = obj.className ? obj.className.split(' ') : []; - - for (var i = 0; i < classes.length; i++) { - if (classes[i] == cls) return; // класс уже есть - } - - classes.push(cls); // добавить - - obj.className = classes.join(' '); // и обновить свойство -} - -var obj = { - className: 'open menu' -}; - -addClass(obj, 'new'); -addClass(obj, 'open'); -addClass(obj, 'me'); -alert(obj.className) // open menu new me -``` - -P.S. "Альтернативный" подход к проверке наличия класса вызовом `obj.className.indexOf(cls)` был бы неверным. В частности, он найдёт `cls = "menu"` в строке классов `obj.className = "open mymenu"`. - -P.P.S. Проверьте, нет ли в вашем решении присвоения `obj.className += " " + cls`. Не добавляет ли оно лишний пробел в случае, если изначально `obj.className = ""`? \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/1-add-class/task.md b/1-js/4-data-structures/8-array-methods/1-add-class/task.md deleted file mode 100644 index 0fdca02c..00000000 --- a/1-js/4-data-structures/8-array-methods/1-add-class/task.md +++ /dev/null @@ -1,23 +0,0 @@ -# Добавить класс в строку - -[importance 5] - -В объекте есть свойство `className`, которое содержит список "классов" - слов, разделенных пробелом: - -```js -var obj = { - className: 'open menu' -} -``` - -Создайте функцию `addClass(obj, cls)`, которая добавляет в список класс `cls`, но только если его там еще нет: - -```js -addClass(obj, 'new'); // obj.className='open menu new' -addClass(obj, 'open'); // без изменений (класс уже существует) -addClass(obj, 'me'); // obj.className='open menu new me' - -alert( obj.className ); // "open menu new me" -``` - -P.S. Ваша функция не должна добавлять лишних пробелов. \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/10-filter-anagrams/_js.view/solution.js b/1-js/4-data-structures/8-array-methods/10-filter-anagrams/_js.view/solution.js deleted file mode 100644 index 857b8f23..00000000 --- a/1-js/4-data-structures/8-array-methods/10-filter-anagrams/_js.view/solution.js +++ /dev/null @@ -1,16 +0,0 @@ -function aclean(arr) { - var obj = {}; - - for (var i = 0; i < arr.length; i++) { - var sorted = arr[i].toLowerCase().split("").sort().join(""); - obj[sorted] = arr[i]; - } - - var result = []; - - for (var key in obj) { - result.push(obj[key]); - } - - return result; -} \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/10-filter-anagrams/_js.view/test.js b/1-js/4-data-structures/8-array-methods/10-filter-anagrams/_js.view/test.js deleted file mode 100644 index c92521c3..00000000 --- a/1-js/4-data-structures/8-array-methods/10-filter-anagrams/_js.view/test.js +++ /dev/null @@ -1,26 +0,0 @@ -function intersection(arr1, arr2) { - return arr1.filter(function(item) { - return arr2.indexOf(item) != -1; - }); -} - -describe("aclean", function() { - - it("содержит ровно по 1 слову из каждого набора анаграмм", function() { - var arr = ["воз", "киборг", "корсет", "зов", "гробик", "костер", "сектор"]; - - var result = aclean(arr); - assert.equal(result.length, 3); - - assert.equal(intersection(result, ["гробик", "киборг"]).length, 1); - assert.equal(intersection(result, ["воз", "зов"]).length, 1); - assert.equal(intersection(result, ["корсет", "сектор", "костер"]).length, 1); - - }); - - it("не различает регистр символов", function() { - var arr = ["воз", "ЗОВ"]; - assert.equal(aclean(arr).length, 1); - }); - -}); \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/10-filter-anagrams/solution.md b/1-js/4-data-structures/8-array-methods/10-filter-anagrams/solution.md deleted file mode 100644 index 60ec5487..00000000 --- a/1-js/4-data-structures/8-array-methods/10-filter-anagrams/solution.md +++ /dev/null @@ -1,70 +0,0 @@ -# Решение - -Чтобы обнаружить анаграммы, разобьём каждое слово на буквы и отсортируем их. В отсортированном по буквам виде все анаграммы одинаковы. - -Например: - -``` -воз, зов -> взо -киборг, гробик -> бгикор -... -``` - -По такой последовательности будем делать массив уникальным. - -Для этого воспользуемся вспомогательным объектом, в который будем записывать слова по отсортированному ключу: - -```js -//+ run -function aclean(arr) { - // этот объект будем использовать для уникальности - var obj = {}; - - for (var i = 0; i < arr.length; i++) { - // разбить строку на буквы, отсортировать и слить обратно -*!* - var sorted = arr[i].toLowerCase().split('').sort().join(''); // (*) -*/!* - - obj[sorted] = arr[i]; // сохраняет только одно значение с таким ключом - } - - var result = []; - - // теперь в obj находится для каждого ключа ровно одно значение - for (var key in obj) result.push(obj[key]); - - return result; -} - -var arr = ["воз", "киборг", "корсет", "ЗОВ", "гробик", "костер", "сектор"]; - -alert( aclean(arr) ); -``` - -Приведение слова к сортированному по буквам виду осуществляется цепочкой вызовов в строке `(*)`. - -Для удобства комментирования разобьём её на несколько строк (JavaScript это позволяет): - -```js -var sorted = arr[i] // ЗОВ - .toLowerCase() // зов - .split('') // ['з','о','в'] - .sort() // ['в','з','о'] - .join(''); // взо -``` - -Получится, что два разных слова `'ЗОВ'` и `'воз'` получат одинаковую отсортированную форму `'взо'`. - -Следующая строка: - -```js -obj[sorted] = arr[i]; -``` - -В объект `obj` будет записано сначала первое из слов `obj['взо'] = "воз"`, а затем `obj['взо'] = 'ЗОВ'`. - -Обратите внимание, ключ -- отсортирован, а само слово -- в исходной форме, чтобы можно было потом получить его из объекта. - -Вторая запись по тому же ключу перезапишет первую, то есть в объекте останется ровно одно слово с таким набором букв. - diff --git a/1-js/4-data-structures/8-array-methods/10-filter-anagrams/task.md b/1-js/4-data-structures/8-array-methods/10-filter-anagrams/task.md deleted file mode 100644 index 9654d440..00000000 --- a/1-js/4-data-structures/8-array-methods/10-filter-anagrams/task.md +++ /dev/null @@ -1,27 +0,0 @@ -# Отфильтровать анаграммы - -[importance 3] - -*Анаграммы* -- слова, состоящие из одинакового количества одинаковых букв, но в разном порядке. -Например: - -``` -воз - зов -киборг - гробик -корсет - костер - сектор -``` - -Напишите функцию `aclean(arr)`, которая возвращает массив слов, очищенный от анаграмм. - -Например: - -```js -var arr = ["воз", "киборг", "корсет", "ЗОВ", "гробик", "костер", "сектор"]; - -alert( aclean(arr) ); // "воз,киборг,корсет" или "ЗОВ,гробик,сектор" -``` - -Из каждой группы анаграмм должно остаться только одно слово, не важно какое. - - - diff --git a/1-js/4-data-structures/8-array-methods/11-array-unique/_js.view/solution.js b/1-js/4-data-structures/8-array-methods/11-array-unique/_js.view/solution.js deleted file mode 100644 index b1b50cc5..00000000 --- a/1-js/4-data-structures/8-array-methods/11-array-unique/_js.view/solution.js +++ /dev/null @@ -1,10 +0,0 @@ -function unique(arr) { - var obj = {}; - - for (var i = 0; i < arr.length; i++) { - var str = arr[i]; - obj[str] = true; // запомнить строку в виде свойства объекта - } - - return Object.keys(obj); // или собрать ключи перебором для IE8- -} \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/11-array-unique/_js.view/test.js b/1-js/4-data-structures/8-array-methods/11-array-unique/_js.view/test.js deleted file mode 100644 index 771317bf..00000000 --- a/1-js/4-data-structures/8-array-methods/11-array-unique/_js.view/test.js +++ /dev/null @@ -1,15 +0,0 @@ -describe("unique", function() { - it("убирает неуникальные элементы из массива", function() { - var strings = ["кришна", "кришна", "харе", "харе", - "харе", "харе", "кришна", "кришна", "8-()" - ]; - - assert.deepEqual(unique(strings), ["кришна", "харе", "8-()"]); - }); - - it("не изменяет исходный массив", function() { - var strings = ["кришна", "кришна", "харе", "харе"]; - unique(strings); - assert.deepEqual(strings, ["кришна", "кришна", "харе", "харе"]); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/11-array-unique/solution.md b/1-js/4-data-structures/8-array-methods/11-array-unique/solution.md deleted file mode 100644 index e49f17b2..00000000 --- a/1-js/4-data-structures/8-array-methods/11-array-unique/solution.md +++ /dev/null @@ -1,82 +0,0 @@ -# Решение перебором (медленное) - -Пройдём по массиву вложенным циклом. - -Для каждого элемента мы будем искать, был ли такой уже. Если был -- игнорировать: - -```js -//+ run -function unique(arr) { - var result = []; - - nextInput: - for (var i = 0; i < arr.length; i++) { - var str = arr[i]; // для каждого элемента - for (var j = 0; j < result.length; j++) { // ищем, был ли он уже? - if (result[j] == str) continue nextInput; // если да, то следующий - } - result.push(str); - } - - return result; -} - -var strings = ["кришна", "кришна", "харе", "харе", - "харе", "харе", "кришна", "кришна", "8-()" -]; - -alert( unique(strings) ); // кришна, харе, 8-() -``` - -Давайте посмотрим, насколько быстро он будет работать. - -Предположим, в массиве `100` элементов. Если все они одинаковые, то `result` будет состоять из одного элемента и вложенный цикл будет выполняться сразу. В этом случае всё хорошо. - -А если все, или почти все элементы разные? - -В этом случае для каждого элемента понадобится обойти весь текущий массив результатов, после чего -- добавить в этот массив. - -
        -
      1. Для первого элемента -- это обойдётся в `0` операций доступа к элементам `result` (он пока пустой).
      2. -
      3. Для второго элемента -- это обойдётся в `1` операцию доступа к элементам `result`.
      4. -
      5. Для третьего элемента -- это обойдётся в `2` операции доступа к элементам `result`.
      6. -
      7. ...Для n-го элемента -- это обойдётся в `n-1` операций доступа к элементам `result`.
      8. -
      - -Всего 0 + 1 + 2 + ... + n-1 = (n-1)*n/2 = n2/2 - n/2 (как сумма арифметической прогрессии), то есть количество операций растёт примерно как квадрат от `n`. - -Это очень быстрый рост. Для `100` элементов -- `4950` операций, для `1000` -- `499500` (по формуле выше). - -Поэтому такое решение подойдёт только для небольших массивов. Вместо вложенного `for` можно использовать и `arr.indexOf`, ситуация от этого не поменяется, так как `indexOf` тоже ищет перебором. - -# Решение с объектом (быстрое) - -Наилучшая техника для выбора уникальных строк -- использование вспомогательного объекта `obj`. Ведь название свойства в объекте, с одной стороны -- строка, а с другой -- всегда уникально. Повторная запись в свойство с тем же именем перезапишет его. - -Например, если `"харе"` попало в объект один раз (`obj["харе"] = true`), то второе такое же присваивание ничего не изменит. - -Решение ниже создаёт объект `obj = {}` и записывает в него все строки как имена свойств. А затем собирает свойства из объекта в массив через `for..in`. Дубликатов уже не будет. - -```js -//+ run -function unique(arr) { - var obj = {}; - - for (var i = 0; i < arr.length; i++) { - var str = arr[i]; -*!* - obj[str] = true; // запомнить строку в виде свойства объекта -*/!* - } - - return Object.keys(obj); // или собрать ключи перебором для IE8- -} - -var strings = ["кришна", "кришна", "харе", "харе", - "харе", "харе", "кришна", "кришна", "8-()" -]; - -alert( unique(strings) ); // кришна, харе, 8-() -``` - -Так что можно положить все значения как ключи в объект, а потом достать. \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/11-array-unique/task.md b/1-js/4-data-structures/8-array-methods/11-array-unique/task.md deleted file mode 100644 index 15a5e3af..00000000 --- a/1-js/4-data-structures/8-array-methods/11-array-unique/task.md +++ /dev/null @@ -1,22 +0,0 @@ -# Оставить уникальные элементы массива - -[importance 3] - -Пусть `arr` -- массив строк. - -Напишите функцию `unique(arr)`, которая возвращает массив, содержащий только уникальные элементы `arr`. - -Например: - -```js -function unique(arr) { - /* ваш код */ -} - -var strings = ["кришна", "кришна", "харе", "харе", - "харе", "харе", "кришна", "кришна", "8-()" -]; - -alert( unique(strings) ); // кришна, харе, 8-() -``` - diff --git a/1-js/4-data-structures/8-array-methods/2-camelcase/_js.view/solution.js b/1-js/4-data-structures/8-array-methods/2-camelcase/_js.view/solution.js deleted file mode 100644 index 6a150668..00000000 --- a/1-js/4-data-structures/8-array-methods/2-camelcase/_js.view/solution.js +++ /dev/null @@ -1,10 +0,0 @@ -function camelize(str) { - var arr = str.split('-'); - - for (var i = 1; i < arr.length; i++) { - // преобразовать: первый символ с большой буквы - arr[i] = arr[i].charAt(0).toUpperCase() + arr[i].slice(1); - } - - return arr.join(''); -} \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/2-camelcase/_js.view/test.js b/1-js/4-data-structures/8-array-methods/2-camelcase/_js.view/test.js deleted file mode 100644 index 0546d816..00000000 --- a/1-js/4-data-structures/8-array-methods/2-camelcase/_js.view/test.js +++ /dev/null @@ -1,22 +0,0 @@ -describe("camelize", function() { - - it("оставляет пустую строку \"как есть\"", function() { - assert.equal(camelize(""), ""); - }); - - describe("делает заглавным первый символ после дефиса", function() { - - it("превращает background-color в backgroundColor", function() { - assert.equal(camelize("background-color"), "backgroundColor"); - }); - - it("превращает list-style-image в listStyleImage", function() { - assert.equal(camelize("list-style-image"), "listStyleImage"); - }); - - it("превращает -webkit-transition в WebkitTransition", function() { - assert.equal(camelize("-webkit-transition"), "WebkitTransition"); - }); - }); - -}); \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/2-camelcase/solution.md b/1-js/4-data-structures/8-array-methods/2-camelcase/solution.md deleted file mode 100644 index 4f97bbc5..00000000 --- a/1-js/4-data-structures/8-array-methods/2-camelcase/solution.md +++ /dev/null @@ -1,26 +0,0 @@ -# Идея - -Задача может быть решена несколькими способами. Один из них -- разбить строку по дефису `str.split('-')`, затем последовательно сконструировать новую. - -# Решение - -Разобьем строку в массив, а затем преобразуем его элементы и сольём обратно: - -```js -//+ run -function camelize(str) { - var arr = str.split('-'); - - for (var i = 1; i < arr.length; i++) { - // преобразовать: первый символ с большой буквы - arr[i] = arr[i].charAt(0).toUpperCase() + arr[i].slice(1); - } - - return arr.join(''); -} - -alert( camelize("background-color") ); // backgroundColor -alert( camelize("list-style-image") ); // listStyleImage -alert( camelize("-webkit-transition") ); // WebkitTransition -``` - diff --git a/1-js/4-data-structures/8-array-methods/2-camelcase/task.md b/1-js/4-data-structures/8-array-methods/2-camelcase/task.md deleted file mode 100644 index dd2446d4..00000000 --- a/1-js/4-data-structures/8-array-methods/2-camelcase/task.md +++ /dev/null @@ -1,20 +0,0 @@ -# Перевести текст вида border-left-width в borderLeftWidth - -[importance 3] - -Напишите функцию `camelize(str)`, которая преобразует строки вида "my-short-string" в "myShortString". - -То есть, дефисы удаляются, а все слова после них получают заглавную букву. - -Например: - -```js -camelize("background-color") == 'backgroundColor'; -camelize("list-style-image") == 'listStyleImage'; -camelize("-webkit-transition") == 'WebkitTransition'; -``` - -Такая функция полезна при работе с CSS. - -P.S. Вам пригодятся методы строк `charAt`, `split` и `toUpperCase`. - diff --git a/1-js/4-data-structures/8-array-methods/3-remove-class/_js.view/solution.js b/1-js/4-data-structures/8-array-methods/3-remove-class/_js.view/solution.js deleted file mode 100644 index f5c9ab64..00000000 --- a/1-js/4-data-structures/8-array-methods/3-remove-class/_js.view/solution.js +++ /dev/null @@ -1,11 +0,0 @@ -function removeClass(obj, cls) { - var classes = obj.className.split(' '); - - for (i = 0; i < classes.length; i++) { - if (classes[i] == cls) { - classes.splice(i, 1); // удалить класс - i--; - } - } - obj.className = classes.join(' '); -} \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/3-remove-class/_js.view/test.js b/1-js/4-data-structures/8-array-methods/3-remove-class/_js.view/test.js deleted file mode 100644 index 809009a9..00000000 --- a/1-js/4-data-structures/8-array-methods/3-remove-class/_js.view/test.js +++ /dev/null @@ -1,43 +0,0 @@ -describe("removeClass", function() { - - it("ничего не делает, если класса нет", function() { - var obj = { - className: 'open menu' - }; - removeClass(obj, 'new'); - assert.deepEqual(obj, { - className: 'open menu' - }); - }); - - it("не меняет пустое свойство", function() { - var obj = { - className: '' - }; - removeClass(obj, 'new'); - assert.deepEqual(obj, { - className: "" - }); - }); - - it("удаляет класс, не оставляя лишних пробелов", function() { - var obj = { - className: 'open menu' - }; - removeClass(obj, 'open'); - assert.deepEqual(obj, { - className: "menu" - }); - }); - - it("если класс один и он удалён, то результат - пустая строка", function() { - var obj = { - className: "menu" - }; - removeClass(obj, 'menu'); - assert.deepEqual(obj, { - className: "" - }); - }); - -}); \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/3-remove-class/solution.md b/1-js/4-data-structures/8-array-methods/3-remove-class/solution.md deleted file mode 100644 index acade80b..00000000 --- a/1-js/4-data-structures/8-array-methods/3-remove-class/solution.md +++ /dev/null @@ -1,33 +0,0 @@ -Решение заключается в том, чтобы разбить `className` в массив классов, а затем пройтись по нему циклом. Если класс есть - удаляем его `splice`, заново объединяем массив в строку и присваиваем объекту. - -```js -//+ run -function removeClass(obj, cls) { - var classes = obj.className.split(' '); - - for (i = 0; i < classes.length; i++) { - if (classes[i] == cls) { - classes.splice(i, 1); // удалить класс -*!* - i--; // (*) -*/!* - } - } - obj.className = classes.join(' '); - -} - -var obj = { - className: 'open menu menu' -} - -removeClass(obj, 'blabla'); -removeClass(obj, 'menu') -alert(obj.className) // open -``` - -В примере выше есть тонкий момент. Элементы массива проверяются один за другим. При вызове `splice` удаляется текущий, `i-й` элемент, и те элементы, которые идут дальше, сдвигаются на его место. - -Таким образом, **на месте `i` оказывается новый, непроверенный элемент**. - -Чтобы это учесть, строчка `(*)` уменьшает `i`, чтобы следующая итерация цикла заново проверила элемент с номером `i`. Без нее функция будет работать с ошибками. diff --git a/1-js/4-data-structures/8-array-methods/3-remove-class/task.md b/1-js/4-data-structures/8-array-methods/3-remove-class/task.md deleted file mode 100644 index 8643ca06..00000000 --- a/1-js/4-data-structures/8-array-methods/3-remove-class/task.md +++ /dev/null @@ -1,30 +0,0 @@ -# Функция removeClass - -[importance 5] - -У объекта есть свойство `className`, которое хранит список "классов" - слов, разделенных пробелами: - -```js -var obj = { - className: 'open menu' -}; -``` - -Напишите функцию `removeClass(obj, cls)`, которая удаляет класс `cls`, если он есть: - -```js -removeClass(obj, 'open'); // obj.className='menu' -removeClass(obj, 'blabla'); // без изменений (нет такого класса) -``` - -P.S. Дополнительное усложнение. Функция должна корректно обрабатывать дублирование класса в строке: - -```js -obj = { - className: 'my menu menu' -}; -removeClass(obj, 'menu'); -alert( obj.className ); // 'my' -``` - -Лишних пробелов после функции образовываться не должно. \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/4-filter-in-place/_js.view/solution.js b/1-js/4-data-structures/8-array-methods/4-filter-in-place/_js.view/solution.js deleted file mode 100644 index 541e1e0c..00000000 --- a/1-js/4-data-structures/8-array-methods/4-filter-in-place/_js.view/solution.js +++ /dev/null @@ -1,10 +0,0 @@ -function filterRangeInPlace(arr, a, b) { - - for (var i = 0; i < arr.length; i++) { - var val = arr[i]; - if (val < a || val > b) { - arr.splice(i--, 1); - } - } - -} \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/4-filter-in-place/_js.view/test.js b/1-js/4-data-structures/8-array-methods/4-filter-in-place/_js.view/test.js deleted file mode 100644 index f8374db2..00000000 --- a/1-js/4-data-structures/8-array-methods/4-filter-in-place/_js.view/test.js +++ /dev/null @@ -1,9 +0,0 @@ -describe("filterRangeInPlace", function() { - - it("меняет массив, оставляя только значения из диапазона", function() { - var arr = [5, 3, 8, 1]; - filterRangeInPlace(arr, 1, 4); - assert.deepEqual(arr, [3, 1]); - }); - -}); \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/4-filter-in-place/solution.md b/1-js/4-data-structures/8-array-methods/4-filter-in-place/solution.md deleted file mode 100644 index 1815310b..00000000 --- a/1-js/4-data-structures/8-array-methods/4-filter-in-place/solution.md +++ /dev/null @@ -1,21 +0,0 @@ - - -```js -//+ run -function filterRangeInPlace(arr, a, b) { - - for (var i = 0; i < arr.length; i++) { - var val = arr[i]; - if (val < a || val > b) { - arr.splice(i--, 1); - } - } - -} - -var arr = [5, 3, 8, 1]; - -filterRangeInPlace(arr, 1, 4); -alert( arr ); // [3, 1] -``` - diff --git a/1-js/4-data-structures/8-array-methods/4-filter-in-place/task.md b/1-js/4-data-structures/8-array-methods/4-filter-in-place/task.md deleted file mode 100644 index bd826837..00000000 --- a/1-js/4-data-structures/8-array-methods/4-filter-in-place/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# Фильтрация массива "на месте" - -[importance 4] - -Создайте функцию `filterRangeInPlace(arr, a, b)`, которая получает массив с числами `arr` и удаляет из него все числа вне диапазона `a..b`. -То есть, проверка имеет вид `a ≤ arr[i] ≤ b`. Функция должна менять сам массив и ничего не возвращать. - -Например: - -```js -arr = [5, 3, 8, 1]; - -filterRangeInPlace(arr, 1, 4); // удалены числа вне диапазона 1..4 - -alert( arr ); // массив изменился: остались [3, 1] -``` - diff --git a/1-js/4-data-structures/8-array-methods/5-sort-back/solution.md b/1-js/4-data-structures/8-array-methods/5-sort-back/solution.md deleted file mode 100644 index 62f68429..00000000 --- a/1-js/4-data-structures/8-array-methods/5-sort-back/solution.md +++ /dev/null @@ -1,15 +0,0 @@ - - -```js -//+ run -var arr = [5, 2, 1, -10, 8]; - -function compareReversed(a, b) { - return b - a; -} - -arr.sort(compareReversed); - -alert( arr ); -``` - diff --git a/1-js/4-data-structures/8-array-methods/5-sort-back/task.md b/1-js/4-data-structures/8-array-methods/5-sort-back/task.md deleted file mode 100644 index e2976326..00000000 --- a/1-js/4-data-structures/8-array-methods/5-sort-back/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Сортировать в обратном порядке - -[importance 5] - -Как отсортировать массив чисел в обратном порядке? - -```js -var arr = [5, 2, 1, -10, 8]; - -// отсортируйте? - -alert( arr ); // 8, 5, 2, 1, -10 -``` - diff --git a/1-js/4-data-structures/8-array-methods/6-copy-sort-array/solution.md b/1-js/4-data-structures/8-array-methods/6-copy-sort-array/solution.md deleted file mode 100644 index 4d1d8586..00000000 --- a/1-js/4-data-structures/8-array-methods/6-copy-sort-array/solution.md +++ /dev/null @@ -1,14 +0,0 @@ -Для копирования массива используем `slice()`, и тут же -- сортировку: - -```js -//+ run -var arr = ["HTML", "JavaScript", "CSS"]; - -*!* -var arrSorted = arr.slice().sort(); -*/!* - -alert( arrSorted ); -alert( arr ); -``` - diff --git a/1-js/4-data-structures/8-array-methods/6-copy-sort-array/task.md b/1-js/4-data-structures/8-array-methods/6-copy-sort-array/task.md deleted file mode 100644 index 374bbf5c..00000000 --- a/1-js/4-data-structures/8-array-methods/6-copy-sort-array/task.md +++ /dev/null @@ -1,18 +0,0 @@ -# Скопировать и отсортировать массив - -[importance 5] - -Есть массив строк `arr`. Создайте массив `arrSorted` -- из тех же элементов, но отсортированный. - -Исходный массив не должен меняться. - -```js -var arr = ["HTML", "JavaScript", "CSS"]; - -// ... ваш код ... - -alert( arrSorted ); // CSS, HTML, JavaScript -alert( arr ); // HTML, JavaScript, CSS (без изменений) -``` - -Постарайтесь сделать код как можно короче. \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/7-shuffle-array/solution.md b/1-js/4-data-structures/8-array-methods/7-shuffle-array/solution.md deleted file mode 100644 index 559a0cd6..00000000 --- a/1-js/4-data-structures/8-array-methods/7-shuffle-array/solution.md +++ /dev/null @@ -1,23 +0,0 @@ -# Подсказка - -Функция сортировки должна возвращать случайный результат сравнения. Используйте для этого [Math.random](http://javascript.ru/Math.random). - -# Решение - -Обычно `Math.random()` возвращает результат от `0` до `1`. Вычтем `0.5`, чтобы область значений стала `[-0.5 ... 0.5)`. - -```js -//+ run -var arr = [1, 2, 3, 4, 5]; - -*!* -function compareRandom(a, b) { - return Math.random() - 0.5; -} - -arr.sort(compareRandom); -*/!* - -alert( arr ); // элементы в случайном порядке, например [3,5,1,2,4] -``` - diff --git a/1-js/4-data-structures/8-array-methods/7-shuffle-array/task.md b/1-js/4-data-structures/8-array-methods/7-shuffle-array/task.md deleted file mode 100644 index 1dd4e46b..00000000 --- a/1-js/4-data-structures/8-array-methods/7-shuffle-array/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Случайный порядок в массиве - -[importance 3] - -Используйте функцию `sort` для того, чтобы "перетрясти" элементы массива в случайном порядке. - -```js -var arr = [1, 2, 3, 4, 5]; - -arr.sort(ваша функция); - -alert( arr ); // элементы в случайном порядке, например [3,5,1,2,4] -``` - diff --git a/1-js/4-data-structures/8-array-methods/8-sort-objects/solution.md b/1-js/4-data-structures/8-array-methods/8-sort-objects/solution.md deleted file mode 100644 index 21bf6d13..00000000 --- a/1-js/4-data-structures/8-array-methods/8-sort-objects/solution.md +++ /dev/null @@ -1,26 +0,0 @@ -Для сортировки объявим и передадим в `sort` анонимную функцию, которая сравнивает объекты по полю `age`: - -```js -//+ run no-beautify -*!* -// Наша функция сравнения -function compareAge(personA, personB) { - return personA.age - personB.age; -} -*/!* - -// проверка -var vasya = { name: "Вася", age: 23 }; -var masha = { name: "Маша", age: 18 }; -var vovochka = { name: "Вовочка", age: 6 }; - -var people = [ vasya , masha , vovochka ]; - -people.sort(compareAge); - -// вывести -for(var i = 0; i < people.length; i++) { - alert(people[i].name); // Вовочка Маша Вася -} -``` - diff --git a/1-js/4-data-structures/8-array-methods/8-sort-objects/task.md b/1-js/4-data-structures/8-array-methods/8-sort-objects/task.md deleted file mode 100644 index 5a3ae3ec..00000000 --- a/1-js/4-data-structures/8-array-methods/8-sort-objects/task.md +++ /dev/null @@ -1,23 +0,0 @@ -# Сортировка объектов - -[importance 5] - -Напишите код, который отсортирует массив объектов `people` по полю `age`. - -Например: - -```js -//+ no-beautify -var vasya = { name: "Вася", age: 23 }; -var masha = { name: "Маша", age: 18 }; -var vovochka = { name: "Вовочка", age: 6 }; - -var people = [ vasya , masha , vovochka ]; - -... ваш код ... - -// теперь people: [vovochka, masha, vasya] -alert(people[0].age) // 6 -``` - -Выведите список имён в массиве после сортировки. \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/9-output-single-linked-list/linked-list.png b/1-js/4-data-structures/8-array-methods/9-output-single-linked-list/linked-list.png deleted file mode 100644 index 5c1c8842..00000000 Binary files a/1-js/4-data-structures/8-array-methods/9-output-single-linked-list/linked-list.png and /dev/null differ diff --git a/1-js/4-data-structures/8-array-methods/9-output-single-linked-list/linked-list@2x.png b/1-js/4-data-structures/8-array-methods/9-output-single-linked-list/linked-list@2x.png deleted file mode 100644 index 84def76b..00000000 Binary files a/1-js/4-data-structures/8-array-methods/9-output-single-linked-list/linked-list@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/8-array-methods/9-output-single-linked-list/solution.md b/1-js/4-data-structures/8-array-methods/9-output-single-linked-list/solution.md deleted file mode 100644 index 5182f748..00000000 --- a/1-js/4-data-structures/8-array-methods/9-output-single-linked-list/solution.md +++ /dev/null @@ -1,158 +0,0 @@ -# Вывод списка в цикле - -```js -//+ run -var list = { - value: 1, - next: { - value: 2, - next: { - value: 3, - next: { - value: 4, - next: null - } - } - } -}; - -function printList(list) { - var tmp = list; - - while (tmp) { - alert( tmp.value ); - tmp = tmp.next; - } - -} - -printList(list); -``` - -Обратите внимание, что для прохода по списку используется временная переменная `tmp`, а не `list`. Можно было бы и бегать по списку, используя входной параметр функции: - -```js -function printList(list) { - - while(*!*list*/!*) { - alert( list.value ); - list = list.next; - } - -} -``` - -...Но при этом мы в будущем не сможем расширить функцию и сделать со списком что-то ещё, ведь после окончания цикла начало списка уже нигде не хранится. - -Поэтому и используется временная переменная -- чтобы сделать код расширяемым, и, кстати, более понятным, ведь роль `tmp` -- исключительно обход списка, как `i` в цикле `for`. - -# Вывод списка с рекурсией - -Рекурсивный вариант `printList(list)` следует простой логике: вывести текущее значение `(1)`, а затем пропустить через себя следующее `(2)`: - -```js -//+ run -var list = { - value: 1, - next: { - value: 2, - next: { - value: 3, - next: { - value: 4, - next: null - } - } - } -}; - -function printList(list) { - - alert( list.value ); // (1) - - if (list.next) { - printList(list.next); // (2) - } - -} - -printList(list); -``` - -# Обратный вывод с рекурсией - -Обратный вывод -- почти то же самое, что прямой, просто сначала мы обрабатываем следующее значение, а потом -- текущее: - -```js -//+ run -var list = { - value: 1, - next: { - value: 2, - next: { - value: 3, - next: { - value: 4, - next: null - } - } - } -}; - -function printReverseList(list) { - - if (list.next) { - printReverseList(list.next); - } - - alert( list.value ); -} - -printReverseList(list); -``` - -# Обратный вывод без рекурсии - -```js -//+ run -var list = { - value: 1, - next: { - value: 2, - next: { - value: 3, - next: { - value: 4, - next: null - } - } - } -}; - - -function printReverseList(list) { - var arr = []; - var tmp = list; - - while (tmp) { - arr.push(tmp.value); - tmp = tmp.next; - } - - for (var i = arr.length - 1; i >= 0; i--) { - alert( arr[i] ); - } -} - -printReverseList(list); -``` - -**Обратный вывод без рекурсии быстрее.** - -По сути, рекурсивный вариант и нерекурсивный работают одинаково: они проходят список и запоминают его элементы, а потом выводят в обратном порядке. - -В случае с массивом это очевидно, а для рекурсии запоминание происходит в стеке (внутренней специальной структуре данных): когда вызывается вложенная функция, то интерпретатор сохраняет в стек текущие параметры. Вложенные вызовы заполняют стек, а потом он выводится в обратном порядке. - -При этом, при рекурсии в стеке сохраняется не только элемент списка, а другая вспомогательная информация, необходимая для возвращения из вложенного вызова. Поэтому тратится больше памяти. Все эти расходы отсутствуют во варианте без рекурсии, так как в массиве хранится именно то, что нужно. - -Преимущество рекурсии, с другой стороны -- более короткий и, зачастую, более простой код. \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/9-output-single-linked-list/task.md b/1-js/4-data-structures/8-array-methods/9-output-single-linked-list/task.md deleted file mode 100644 index bfb1523f..00000000 --- a/1-js/4-data-structures/8-array-methods/9-output-single-linked-list/task.md +++ /dev/null @@ -1,50 +0,0 @@ -# Вывести односвязный список - -[importance 5] - -[Односвязный список](http://ru.wikipedia.org/wiki/Связный_список) -- это структура данных, которая состоит из *элементов*, каждый из которых хранит ссылку на следующий. Последний элемент может не иметь ссылки, либо она равна `null`. - -Например, объект ниже задаёт односвязный список, в `next` хранится ссылка на следующий элемент: - -```js -var list = { - value: 1, - next: { - value: 2, - next: { - value: 3, - next: { - value: 4, - next: null - } - } - } -}; -``` - -Графическое представление этого списка: - - -Альтернативный способ создания: - -```js -//+ no-beautify -var list = { value: 1 }; -list.next = { value: 2 }; -list.next.next = { value: 3 }; -list.next.next.next = { value: 4 }; -``` - -Такая структура данных интересна тем, что можно очень быстро разбить список на части, объединить списки, удалить или добавить элемент в любое место, включая начало. При использовании массива такие действия требуют обширных перенумерований. - -Задачи: - -
        -
      1. Напишите функцию `printList(list)`, которая выводит элементы списка по очереди, при помощи цикла.
      2. -
      3. Напишите функцию `printList(list)` при помощи рекурсии.
      4. -
      5. Напишите функцию `printReverseList(list)`, которая выводит элементы списка в обратном порядке, при помощи рекурсии. -Для списка выше она должна выводить `4`,`3`,`2`,`1`
      6. -
      7. Сделайте вариант `printReverseList(list)`, использующий не рекурсию, а цикл.
      8. -
      - -Как лучше -- с рекурсией или без? \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-methods/article.md b/1-js/4-data-structures/8-array-methods/article.md deleted file mode 100644 index 331738e6..00000000 --- a/1-js/4-data-structures/8-array-methods/article.md +++ /dev/null @@ -1,441 +0,0 @@ -# Массивы: методы - -В этой главе мы рассмотрим встроенные методы массивов JavaScript. -[cut] - -## Метод split - -Ситуация из реальной жизни. Мы пишем сервис отсылки сообщений и посетитель вводит имена тех, кому его отправить: `Маша, Петя, Марина, Василий...`. Но нам-то гораздо удобнее работать с массивом имен, чем с одной строкой. - -К счастью, есть метод `split(s)`, который позволяет превратить строку в массив, разбив ее по разделителю `s`. В примере ниже таким разделителем является строка из запятой и пробела. - -```js -//+ run -var names = 'Маша, Петя, Марина, Василий'; - -var arr = names.split(', '); - -for (var i = 0; i < arr.length; i++) { - alert( 'Вам сообщение ' + arr[i] ); -} -``` - -[smart header="Второй аргумент `split`"] -У метода `split` есть необязательный второй аргумент -- ограничение на количество элементов в массиве. Если их больше, чем указано -- остаток массива будет отброшен: - -```js -//+ run -alert( "a,b,c,d".split(',', *!*2*/!*) ); // a,b -``` - -[/smart] - -[smart header="Разбивка по буквам"] -Вызов `split` с пустой строкой разобьёт по буквам: - -```js -//+ run -var str = "тест"; - -alert( str.split('') ); // т,е,с,т -``` - -[/smart] - -## Метод join - -Вызов `arr.join(str)` делает в точности противоположное `split`. Он берет массив и склеивает его в строку, используя `str` как разделитель. - -Например: - -```js -//+ run -var arr = ['Маша', 'Петя', 'Марина', 'Василий']; - -var str = arr.join(';'); - -alert( str ); // Маша;Петя;Марина;Василий -``` - -[smart header="new Array + join = Повторение строки"] -Код для повторения строки `3` раза: - -```js -//+ run -alert( new Array(4).join("ля") ); // ляляля -``` - -Как видно, `new Array(4)` делает массив без элементов длины 4, который `join` объединяет в строку, вставляя *между его элементами* строку `"ля"`. - -В результате, так как элементы пусты, получается повторение строки. Такой вот небольшой трюк. -[/smart] - - -## Удаление из массива - -Так как массивы являются объектами, то для удаления ключа можно воспользоваться обычным `delete`: - -```js -//+ run -var arr = ["Я", "иду", "домой"]; - -delete arr[1]; // значение с индексом 1 удалено - -// теперь arr = ["Я", undefined, "домой"]; -alert( arr[1] ); // undefined -``` - -Да, элемент удален из массива, но не так, как нам этого хочется. Образовалась "дырка". - -Это потому, что оператор `delete` удаляет пару "ключ-значение". Это -- все, что он делает. Обычно же при удалении из массива мы хотим, чтобы оставшиеся элементы сдвинулись и заполнили образовавшийся промежуток. - -Поэтому для удаления используются специальные методы: из начала -- `shift`, с конца -- `pop`, а из середины -- `splice`, с которым мы сейчас познакомимся. - -## Метод splice - -Метод `splice` -- это универсальный раскладной нож для работы с массивами. Умеет все: удалять элементы, вставлять элементы, заменять элементы -- по очереди и одновременно. - -Его синтаксис: - -
      -
      `arr.splice(index[, deleteCount, elem1, ..., elemN])`
      -
      Удалить `deleteCount` элементов, начиная с номера `index`, а затем вставить `elem1, ..., elemN` на их место. Возвращает массив из удалённых элементов.
      -
      - -Этот метод проще всего понять, рассмотрев примеры. - -Начнём с удаления: - -```js -//+ run -var arr = ["Я", "изучаю", "JavaScript"]; - -*!* -arr.splice(1, 1); // начиная с позиции 1, удалить 1 элемент -*/!* - -alert( arr ); // осталось ["Я", "JavaScript"] -``` - -В следующем примере мы удалим 3 элемента и вставим другие на их место: - -```js -//+ run -var arr = [*!*"Я", "сейчас", "изучаю",*/!* "JavaScript"]; - -// удалить 3 первых элемента и добавить другие вместо них -arr.splice(0, 3, "Мы", "изучаем") - -alert( arr ) // теперь [*!*"Мы", "изучаем"*/!*, "JavaScript"] -``` - -Здесь видно, что `splice` возвращает массив из удаленных элементов: - -```js -//+ run -var arr = [*!*"Я", "сейчас",*/!* "изучаю", "JavaScript"]; - -// удалить 2 первых элемента -var removed = arr.splice(0, 2); - -alert( removed ); // "Я", "сейчас" <-- array of removed elements -``` - -Метод `splice` также может вставлять элементы без удаления, для этого достаточно установить `deleteCount` в `0`: - -```js -//+ run -var arr = ["Я", "изучаю", "JavaScript"]; - -// с позиции 2 -// удалить 0 -// вставить "сложный", "язык" -arr.splice(2, 0, "сложный", "язык"); - -alert( arr ); // "Я", "изучаю", "сложный", "язык", "JavaScript" -``` - -Допускается использование отрицательного номера позиции, которая в этом случае отсчитывается с конца: - -```js -//+ run -var arr = [1, 2, 5] - -// начиная с позиции индексом -1 (предпоследний элемент) -// удалить 0 элементов, -// затем вставить числа 3 и 4 -arr.splice(-1, 0, 3, 4); - -alert( arr ); // результат: 1,2,3,4,5 -``` - -## Метод slice - -Метод `slice(begin, end)` копирует участок массива от `begin` до `end`, не включая `end`. Исходный массив при этом не меняется. - -Например: - -```js -//+ run -var arr = ["Почему", "надо", "учить", "JavaScript"]; - -var arr2 = arr.slice(1, 3); // элементы 1, 2 (не включая 3) - -alert( arr2 ); // надо, учить -``` - -Аргументы ведут себя так же, как и в строковом `slice`: -
        -
      • Если не указать `end` -- копирование будет до конца массива: - -```js -//+ run -var arr = ["Почему", "надо", "учить", "JavaScript"]; - -alert( arr.slice(1) ); // взять все элементы, начиная с номера 1 -``` - -
      • -
      • Можно использовать отрицательные индексы, они отсчитываются с конца: - -```js -var arr2 = arr.slice(-2); // копировать от 2го элемента с конца и дальше -``` - -
      • -
      • Если вообще не указать аргументов -- скопируется весь массив: - -```js -var fullCopy = arr.slice(); -``` - -
      • -
      - -[smart header="Совсем как в строках"] -Синтаксис метода `slice` одинаков для строк и для массивов. Тем проще его запомнить. -[/smart] - -## Сортировка, метод sort(fn) - -Метод `sort()` сортирует массив *на месте*. Например: - -```js -//+ run -var arr = [ 1, 2, 15 ]; - -arr.sort(); - -alert( arr ); // *!*1, 15, 2*/!* -``` - -Не заметили ничего странного в этом примере? - -Порядок стал `1, 15, 2`, это точно не сортировка чисел. Почему? - -**Это произошло потому, что по умолчанию `sort` сортирует, преобразуя элементы к строке.** - -Поэтому и порядок у них строковый, ведь `"2" > "15"`. - -### Свой порядок сортировки - -Для указания своего порядка сортировки в метод `arr.sort(fn)` нужно передать функцию `fn` от двух элементов, которая умеет сравнивать их. - -Внутренний алгоритм функции сортировки умеет сортировать любые массивы -- апельсинов, яблок, пользователей, и тех и других и третьих -- чего угодно. Но для этого ему нужно знать, как их сравнивать. Эту роль и выполняет `fn`. - -Если эту функцию не указать, то элементы сортируются как строки. - -Например, укажем эту функцию явно, отсортируем элементы массива как числа: - -```js -//+ run -function compareNumeric(a, b) { - if (a > b) return 1; - if (a < b) return -1; -} - -var arr = [ 1, 2, 15 ]; - -*!* -arr.sort(compareNumeric); -*/!* - -alert(arr); // *!*1, 2, 15*/!* -``` - -Обратите внимание, мы передаём в `sort()` именно саму функцию `compareNumeric`, без вызова через скобки. Был бы ошибкой следующий код: - -```js -arr.sort( compareNumeric*!*()*/!* ); // не сработает -``` - -Как видно из примера выше, функция, передаваемая `sort`, должна иметь два аргумента. - -Алгоритм сортировки, встроенный в JavaScript, будет передавать ей для сравнения элементы массива. Она должна возвращать: - -
        -
      • Положительное значение, если `a > b`,
      • -
      • Отрицательное значение, если `a < b`,
      • -
      • Если равны -- можно `0`, но вообще -- не важно, что возвращать, их взаимный порядок не имеет значения.
      • -
      - - -[smart header="Алгоритм сортировки"] -В методе `sort`, внутри самого интерпретатора JavaScript, реализован универсальный алгоритм сортировки. Как правило, это ["\"быстрая сортировка\""](http://algolist.manual.ru/sort/quick_sort.php), дополнительно оптимизированная для небольших массивов. - -Он решает, какие пары элементов и когда сравнивать, чтобы отсортировать побыстрее. Мы даём ему функцию -- способ сравнения, дальше он вызывает её сам. - -Кстати, те значения, с которыми `sort` вызывает функцию сравнения, можно увидеть, если вставить в неё `alert`: - -```js -//+ run -[1, -2, 15, 2, 0, 8].sort(function(a, b) { - alert( a + " <> " + b ); -}); -``` - -[/smart] - -[smart header="Сравнение `compareNumeric` в одну строку"] -Функцию `compareNumeric` для сравнения элементов-чисел можно упростить до одной строчки. - -```js -function compareNumeric(a, b) { - return a - b; -} -``` - -Эта функция вполне подходит для `sort`, так как возвращает положительное число, если `a > b`, отрицательное, если наоборот, и `0`, если числа равны. -[/smart] - - -## reverse - -Метод [arr.reverse()](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/reverse) меняет порядок элементов в массиве на обратный. - -```js -//+ run -var arr = [1, 2, 3]; -arr.reverse(); - -alert( arr ); // 3,2,1 -``` - -## concat - -Метод [arr.concat(value1, value2, ... valueN)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/concat) создаёт новый массив, в который копируются элементы из `arr`, а также `value1, value2, ... valueN`. - -Например: - -```js -//+ run -var arr = [1, 2]; -*!* -var newArr = arr.concat(3, 4); -*/!* - -alert( newArr ); // 1,2,3,4 -``` - -У `concat` есть одна забавная особенность. - -Если аргумент `concat` -- массив, то `concat` добавляет элементы из него. - -Например: - -```js -//+ run -var arr = [1, 2]; - -*!* -var newArr = arr.concat([3, 4], 5); // то же самое, что arr.concat(3,4,5) -*/!* - -alert( newArr ); // 1,2,3,4,5 -``` - -## indexOf/lastIndexOf - -Эти методы не поддерживаются в IE8-. Для их поддержки подключите библиотеку [ES5-shim](https://github.com/kriskowal/es5-shim). - - -Метод ["arr.indexOf(searchElement[, fromIndex])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf) возвращает номер элемента `searchElement` в массиве `arr` или `-1`, если его нет. - -Поиск начинается с номера `fromIndex`, если он указан. Если нет -- с начала массива. - -**Для поиска используется строгое сравнение `===`.** - -Например: - -```js -//+ run -var arr = [1, 0, false]; - -alert( arr.indexOf(0) ); // 1 -alert( arr.indexOf(false) ); // 2 -alert( arr.indexOf(null) ); // -1 -``` - -Как вы могли заметить, по синтаксису он полностью аналогичен методу [indexOf для строк](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/indexOf). - -Метод ["arr.lastIndexOf(searchElement[, fromIndex])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/lastIndexOf) ищет справа-налево: с конца массива или с номера `fromIndex`, если он указан. - -[warn header="Методы `indexOf/lastIndexOf` осуществляют поиск перебором"] -Если нужно проверить, существует ли значение в массиве -- его нужно перебрать. Только так. Внутренняя реализация `indexOf/lastIndexOf` осуществляет полный перебор, аналогичный циклу `for` по массиву. Чем длиннее массив, тем дольше он будет работать. -[/warn] - -[smart header="Коллекция уникальных элементов"] -Рассмотрим задачу -- есть коллекция строк, и нужно быстро проверять: есть ли в ней какой-то элемент. Массив для этого не подходит из-за медленного `indexOf`. Но подходит объект! Доступ к свойству объекта осуществляется очень быстро, так что можно сделать все элементы ключами объекта и проверять, есть ли уже такой ключ. - -Например, организуем такую проверку для коллекции строк `"div"`, `"a"` и `"form"`: - -```js -var store = {}; // объект для коллекции - -var items = ["div", "a", "form"]; - -for (var i = 0; i < items.length; i++) { - var key = items[i]; // для каждого элемента создаём свойство - store[key] = true; // значение здесь не важно -} -``` - -Теперь для проверки, есть ли ключ `key`, достаточно выполнить `if (store[key])`. Если есть -- можно использовать значение, если нет -- добавить. - -Такое решение работает только со строками, но применимо к любым элементам, для которых можно вычислить строковый "уникальный ключ". -[/smart] - - - - -## Object.keys(obj) - -Ранее мы говорили о том, что свойства объекта можно перебрать в цикле `for..in`. - -Если мы хотим работать с ними в виде массива, то к нашим услугам -- замечательный метод [Object.keys(obj)](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys). Он поддерживается везде, кроме IE8-: - -```js -//+ run -var user = { - name: "Петя", - age: 30 -} - -var keys = Object.keys(user); - -alert( keys ); // name, age -``` - -## Итого - -Методы: -
        -
      • `push/pop`, `shift/unshift`, `splice` -- для добавления и удаления элементов.
      • -
      • `join/split` -- для преобразования строки в массив и обратно.
      • -
      • `sort` -- для сортировки массива. Если не передать функцию сравнения -- сортирует элементы как строки.
      • -
      • `reverse` -- меняет порядок элементов на обратный.
      • -
      • `concat` -- объединяет массивы.
      • -
      • `indexOf/lastIndexOf` -- возвращают позицию элемента в массиве (не поддерживается в IE8-).
      • -
      - -Изученных нами методов достаточно в 95% случаях, но существуют и другие. Для знакомства с ними рекомендуется заглянуть в справочник Array и [Array в Mozilla Developer Network](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array). \ No newline at end of file diff --git a/1-js/4-data-structures/9-array-iteration/1-rewrite-for-map/solution.md b/1-js/4-data-structures/9-array-iteration/1-rewrite-for-map/solution.md deleted file mode 100644 index 3b1d135e..00000000 --- a/1-js/4-data-structures/9-array-iteration/1-rewrite-for-map/solution.md +++ /dev/null @@ -1,15 +0,0 @@ - - -```js -//+ run -var arr = ["Есть", "жизнь", "на", "Марсе"]; - -*!* -var arrLength = arr.map(function(item) { - return item.length; -}); -*/!* - -alert( arrLength ); // 4,5,2,5 -``` - diff --git a/1-js/4-data-structures/9-array-iteration/1-rewrite-for-map/task.md b/1-js/4-data-structures/9-array-iteration/1-rewrite-for-map/task.md deleted file mode 100644 index db58c32f..00000000 --- a/1-js/4-data-structures/9-array-iteration/1-rewrite-for-map/task.md +++ /dev/null @@ -1,21 +0,0 @@ -# Перепишите цикл через map - -[importance 5] - -Код ниже получает из массива строк новый массив, содержащий их длины: - -```js -//+ run -var arr = ["Есть", "жизнь", "на", "Марсе"]; - -*!* -var arrLength = []; -for (var i = 0; i < arr.length; i++) { - arrLength[i] = arr[i].length; -} -*/!* - -alert( arrLength ); // 4,5,2,5 -``` - -Перепишите выделенный участок: уберите цикл, используйте вместо него метод `map`. \ No newline at end of file diff --git a/1-js/4-data-structures/9-array-iteration/2-partial-sums-array/_js.view/solution.js b/1-js/4-data-structures/9-array-iteration/2-partial-sums-array/_js.view/solution.js deleted file mode 100644 index c79d8e9e..00000000 --- a/1-js/4-data-structures/9-array-iteration/2-partial-sums-array/_js.view/solution.js +++ /dev/null @@ -1,12 +0,0 @@ -function getSums(arr) { - var result = []; - if (!arr.length) return result; - - var totalSum = arr.reduce(function(sum, item) { - result.push(sum); - return sum + item; - }); - result.push(totalSum); - - return result; -} \ No newline at end of file diff --git a/1-js/4-data-structures/9-array-iteration/2-partial-sums-array/_js.view/test.js b/1-js/4-data-structures/9-array-iteration/2-partial-sums-array/_js.view/test.js deleted file mode 100644 index 74a170a6..00000000 --- a/1-js/4-data-structures/9-array-iteration/2-partial-sums-array/_js.view/test.js +++ /dev/null @@ -1,18 +0,0 @@ -describe("getSums", function() { - - it("частичные суммы [1,2,3,4,5] равны [1,3,6,10,15]", function() { - assert.deepEqual(getSums([1, 2, 3, 4, 5]), [1, 3, 6, 10, 15]); - }); - - it("частичные суммы [-2,-1,0,1] равны [-2,-3,-3,-2]", function() { - assert.deepEqual(getSums([-2, -1, 0, 1]), [-2, -3, -3, -2]); - }); - - it("частичные суммы [] равны []", function() { - assert.deepEqual(getSums([]), []); - }); - - it("частичные суммы [1] равны [1]", function() { - assert.deepEqual(getSums([1]), [1]); - }); -}); \ No newline at end of file diff --git a/1-js/4-data-structures/9-array-iteration/2-partial-sums-array/solution.md b/1-js/4-data-structures/9-array-iteration/2-partial-sums-array/solution.md deleted file mode 100644 index 33a25c84..00000000 --- a/1-js/4-data-structures/9-array-iteration/2-partial-sums-array/solution.md +++ /dev/null @@ -1,47 +0,0 @@ -Метод `arr.reduce` подходит здесь идеально. Достаточно пройтись по массиву слева-направа, накапливая текущую сумму в переменной и, кроме того, добавляя её в результирующий массив. - -Неправильный вариант может выглядеть так: - -```js -//+ run -function getSums(arr) { - var result = []; - if (!arr.length) return result; - - arr.reduce(function(sum, item) { - result.push(sum); - return sum + item; - }); - - return result; -} - -alert(getSums([1,2,3,4,5])); // результат: *!*1,3,6,10*/!* -``` - -Перед тем, как читать дальше, посмотрите на него внимательно. Заметили, в чём ошибка? - -Если вы его запустите, то обнаружите, что результат не совсем тот. В получившемся массиве всего четыре элемента, отсутствует последняя сумма. - -Это из-за того, что последняя сумма является результатом метода `reduce`, он на ней заканчивает проход и далее функцию не вызывает, поэтому она оказывается не добавленной в `result`. - -Исправим это: - -```js -//+ run -function getSums(arr) { - var result = []; - - *!*var totalSum*/!* = arr.reduce(function(sum, item) { - result.push(sum); - return sum + item; - }); - *!*result.push(totalSum);*/!* - - return result; -} - -alert(getSums([1,2,3,4,5])); // *!*1,3,6,10,15*/!* -alert(getSums([-2,-1,0,1])); // *!*-2,-3,-3,-2*/!* -``` - diff --git a/1-js/4-data-structures/9-array-iteration/2-partial-sums-array/task.md b/1-js/4-data-structures/9-array-iteration/2-partial-sums-array/task.md deleted file mode 100644 index a5dffc9e..00000000 --- a/1-js/4-data-structures/9-array-iteration/2-partial-sums-array/task.md +++ /dev/null @@ -1,24 +0,0 @@ -# Массив частичных сумм - -[importance 2] - -На входе массив чисел, например: `arr = [1,2,3,4,5]`. - -Напишите функцию `getSums(arr)`, которая возвращает массив его частичных сумм. - -Иначе говоря, вызов `getSums(arr)` должен возвращать новый массив из такого же числа элементов, в котором на каждой позиции должна быть сумма элементов `arr` до этой позиции включительно. - -То есть: - -```js -//+ no-beautify -для arr = [ 1, 2, 3, 4, 5 ] -getSums( arr ) = [ 1, 1+2, 1+2+3, 1+2+3+4, 1+2+3+4+5 ] = [ 1, 3, 6, 10, 15 ] -``` - -Еще пример: `getSums([-2,-1,0,1]) = [-2,-3,-3,-2]`. - -
        -
      • Функция не должна модифицировать входной массив.
      • -
      • В решении используйте метод `arr.reduce`.
      • -
      diff --git a/1-js/4-data-structures/9-array-iteration/article.md b/1-js/4-data-structures/9-array-iteration/article.md deleted file mode 100644 index 52de853d..00000000 --- a/1-js/4-data-structures/9-array-iteration/article.md +++ /dev/null @@ -1,235 +0,0 @@ -# Массив: перебирающие методы - -Современный стандарт JavaScript предоставляет много методов для "умного" перебора массивов, которые есть в современных браузерах... - -...Ну а для их поддержки в IE8- просто подключите библиотеку [ES5-shim](https://github.com/kriskowal/es5-shim). -[cut] -## forEach - -Метод ["arr.forEach(callback[, thisArg])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach) используется для перебора массива. - -Он для каждого элемента массива вызывает функцию `callback`. - -Этой функции он передаёт три параметра `callback(item, i, arr)`: - -
        -
      • `item` -- очередной элемент массива.
      • -
      • `i` -- его номер.
      • -
      • `arr` -- массив, который перебирается.
      • -
      - -Например: - -```js -//+ run -var arr = ["Яблоко", "Апельсин", "Груша"]; - -arr.forEach(function(item, i, arr) { - alert( i + ": " + item + " (массив:" + arr + ")" ); -}); -``` - -Второй, необязательный аргумент `forEach` позволяет указать контекст `this` для `callback`. Мы обсудим его в деталях чуть позже, сейчас он нам не важен. - -Метод `forEach` ничего не возвращает, его используют только для перебора, как более "элегантный" вариант, чем обычный цикл `for`. - -## filter - -Метод ["arr.filter(callback[, thisArg])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/filter) используется для *фильтрации* массива через функцию. - -Он создаёт новый массив, в который войдут только те элементы `arr`, для которых вызов `callback(item, i, arr)` возвратит `true`. - -Например: - -```js -//+ run -var arr = [1, -1, 2, -2, 3]; - -*!* -var positiveArr = arr.filter(function(number) { - return number > 0; -}); -*/!* - -alert( positiveArr ); // 1,2,3 -``` - -## map - -Метод ["arr.map(callback[, thisArg])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/map) используется для *трансформации* массива. - -Он создаёт новый массив, который будет состоять из результатов вызова `callback(item, i, arr)` для каждого элемента `arr`. - -Например: - -```js -//+ run -var names = ['HTML', 'CSS', 'JavaScript']; - -*!* -var nameLengths = names.map(function(name) { - return name.length; -}); -*/!* - -// получили массив с длинами -alert( nameLengths ); // 4,3,10 -``` - -## every/some - -Эти методы используется для проверки массива. - -
        -
      • Метод ["arr.every(callback[, thisArg])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/every) возвращает `true`, если вызов `callback` вернёт `true` для *каждого* элемента `arr`.
      • -
      • Метод ["arr.some(callback[, thisArg])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/some) возвращает `true`, если вызов `callback` вернёт `true` для *какого-нибудь* элемента `arr`.
      • -
      - -```js -//+ run -var arr = [1, -1, 2, -2, 3]; - -function isPositive(number) { - return number > 0; -} - -*!* -alert( arr.every(isPositive) ); // false, не все положительные -alert( arr.some(isPositive) ); // true, есть хоть одно положительное -*/!* -``` - -## reduce/reduceRight - -Метод ["arr.reduce(callback[, initialValue])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/reduce) используется для последовательной обработки каждого элемента массива с сохранением промежуточного результата. - -Это один из самых сложных методов для работы с массивами. Но его стоит освоить, потому что временами с его помощью можно в несколько строк решить задачу, которая иначе потребовала бы в разы больше места и времени. - -Метод `reduce` используется для вычисления на основе массива какого-либо единого значения, иначе говорят "для свёртки массива". Чуть далее мы разберём пример для вычисления суммы. - -Он применяет функцию `callback` по очереди к каждому элементу массива слева направо, сохраняя при этом промежуточный результат. - -Аргументы функции `callback(previousValue, currentItem, index, arr)`: - -
        -
      • `previousValue` -- последний результат вызова функции, он же "промежуточный результат".
      • -
      • `currentItem` -- текущий элемент массива, элементы перебираются по очереди слева-направо.
      • -
      • `index` -- номер текущего элемента.
      • -
      • `arr` -- обрабатываемый массив.
      • -
      - -Кроме `callback`, методу можно передать "начальное значение" -- аргумент `initialValue`. Если он есть, то на первом вызове значение `previousValue` будет равно `initialValue`, а если у `reduce` нет второго аргумента, то оно равно первому элементу массива, а перебор начинается со второго. - -Проще всего понять работу метода `reduce` на примере. - -Например, в качестве "свёртки" мы хотим получить сумму всех элементов массива. - -Вот решение в одну строку: - -```js -//+ run -var arr = [1, 2, 3, 4, 5] - -// для каждого элемента массива запустить функцию, -// промежуточный результат передавать первым аргументом далее -var result = arr.reduce(function(sum, current) { - return sum + current; -}, 0); - -alert( result ); // 15 -``` - -Разберём, что в нём происходит. - -При первом запуске `sum` -- исходное значение, с которого начинаются вычисления, равно нулю (второй аргумент `reduce`). - -Сначала анонимная функция вызывается с этим начальным значением и первым элементом массива, результат запоминается и передаётся в следующий вызов, уже со вторым аргументом массива, затем новое значение участвует в вычислениях с третьим аргументом и так далее. - -Поток вычислений получается такой - - - -В виде таблицы где каждая строка -- вызов функции на очередном элементе массива: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      `sum``current`результат
      первый вызов`0``1``1`
      второй вызов`1``2``3`
      третий вызов`3``3``6`
      четвёртый вызов`6``4``10`
      пятый вызов`10``5``15`
      - -Как видно, результат предыдущего вызова передаётся в первый аргумент следующего. - -Кстати, полный набор аргументов функции для `reduce` включает в себя `function(sum, current, i, array)`, то есть номер текущего вызова `i` и весь массив `arr`, но здесь в них нет нужды. - -Посмотрим, что будет, если не указать `initialValue` в вызове `arr.reduce`: - -```js -//+ run -var arr = [1, 2, 3, 4, 5] - -// убрали 0 в конце -var result = arr.reduce(function(sum, current) { - return sum + current -}); - -alert( result ); // 15 -``` - -Результат -- точно такой же! Это потому, что при отсутствии `initialValue` в качестве первого значения берётся первый элемент массива, а перебор стартует со второго. - -Таблица вычислений будет такая же, за вычетом первой строки. - -**Метод [arr.reduceRight](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/reduceRight) работает аналогично, но идёт по массиву справа-налево:** - - - -## Итого - -Мы рассмотрели методы: -
        -
      • `forEach` -- для *перебора* массива.
      • -
      • `filter` -- для *фильтрации* массива.
      • -
      • `every/some` -- для *проверки* массива.
      • -
      • `map` -- для *трансформации* массива в массив.
      • -
      • `reduce/reduceRight` -- для *прохода по массиву с вычислением значения*.
      • -
      - -Во многих ситуациях их использование позволяет написать код короче и понятнее, чем обычный перебор через `for`. - \ No newline at end of file diff --git a/1-js/4-data-structures/9-array-iteration/reduce.png b/1-js/4-data-structures/9-array-iteration/reduce.png deleted file mode 100644 index ddc8d140..00000000 Binary files a/1-js/4-data-structures/9-array-iteration/reduce.png and /dev/null differ diff --git a/1-js/4-data-structures/9-array-iteration/reduce@2x.png b/1-js/4-data-structures/9-array-iteration/reduce@2x.png deleted file mode 100644 index d74a0295..00000000 Binary files a/1-js/4-data-structures/9-array-iteration/reduce@2x.png and /dev/null differ diff --git a/1-js/4-data-structures/index.md b/1-js/4-data-structures/index.md deleted file mode 100644 index 774dee46..00000000 --- a/1-js/4-data-structures/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Структуры данных - -Изучаем JavaScript: расширенное знакомство со встроенными типами данных, их особенностями. \ No newline at end of file diff --git a/1-js/5-functions-closures/1-global-object/1-window-and-variable/solution.md b/1-js/5-functions-closures/1-global-object/1-window-and-variable/solution.md deleted file mode 100644 index 4c3693d0..00000000 --- a/1-js/5-functions-closures/1-global-object/1-window-and-variable/solution.md +++ /dev/null @@ -1,24 +0,0 @@ -Ответ: `1`. - -```js -//+ run untrusted refresh -if ("a" in window) { - var a = 1; -} -alert( a ); -``` - -Посмотрим, почему. - -На стадии подготовки к выполнению, из `var a` создается `window.a`: - -```js -// window = {a:undefined} - -if ("a" in window) { // в if видно что window.a уже есть - var a = 1; // поэтому эта строка сработает -} -alert( a ); -``` - -В результате `a` становится `1`. \ No newline at end of file diff --git a/1-js/5-functions-closures/1-global-object/1-window-and-variable/task.md b/1-js/5-functions-closures/1-global-object/1-window-and-variable/task.md deleted file mode 100644 index d477b705..00000000 --- a/1-js/5-functions-closures/1-global-object/1-window-and-variable/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Window и переменная - -[importance 5] - -Каков будет результат кода? - -```js -if ("a" in window) { - var a = 1; -} -alert( a ); -``` - diff --git a/1-js/5-functions-closures/1-global-object/2-window-and-variable-2/solution.md b/1-js/5-functions-closures/1-global-object/2-window-and-variable-2/solution.md deleted file mode 100644 index d21c6d8d..00000000 --- a/1-js/5-functions-closures/1-global-object/2-window-and-variable-2/solution.md +++ /dev/null @@ -1,12 +0,0 @@ -Ответ: **ошибка**. - -Переменной `a` нет, так что условие `"a" in window` не выполнится. В результате на последней строчке - обращение к неопределенной переменной. - -```js -//+ run untrusted refresh -if ("a" in window) { - a = 1; -} -alert( a ); // <-- error! -``` - diff --git a/1-js/5-functions-closures/1-global-object/2-window-and-variable-2/task.md b/1-js/5-functions-closures/1-global-object/2-window-and-variable-2/task.md deleted file mode 100644 index 70eb2813..00000000 --- a/1-js/5-functions-closures/1-global-object/2-window-and-variable-2/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Window и переменная 2 - -[importance 5] - -Каков будет результат (перед `a` нет `var`)? - -```js -if ("a" in window) { - a = 1; -} -alert( a ); -``` - diff --git a/1-js/5-functions-closures/1-global-object/3-window-and-variable-3/solution.md b/1-js/5-functions-closures/1-global-object/3-window-and-variable-3/solution.md deleted file mode 100644 index 69bbd29b..00000000 --- a/1-js/5-functions-closures/1-global-object/3-window-and-variable-3/solution.md +++ /dev/null @@ -1,14 +0,0 @@ -Ответ: `1`. - -Переменная `a` создается до начала выполнения кода, так что условие `"a" in window` выполнится и сработает `a = 1`. - -```js -//+ run untrusted refresh -if ("a" in window) { - a = 1; -} -var a; - -alert( a ); // 1 -``` - diff --git a/1-js/5-functions-closures/1-global-object/3-window-and-variable-3/task.md b/1-js/5-functions-closures/1-global-object/3-window-and-variable-3/task.md deleted file mode 100644 index 42eabb2e..00000000 --- a/1-js/5-functions-closures/1-global-object/3-window-and-variable-3/task.md +++ /dev/null @@ -1,15 +0,0 @@ -# Window и переменная 3 - -[importance 5] - -Каков будет результат (перед `a` нет `var`, а ниже есть)? - -```js -if ("a" in window) { - a = 1; -} -var a; - -alert( a ); -``` - diff --git a/1-js/5-functions-closures/1-global-object/article.md b/1-js/5-functions-closures/1-global-object/article.md deleted file mode 100644 index b31d40dc..00000000 --- a/1-js/5-functions-closures/1-global-object/article.md +++ /dev/null @@ -1,233 +0,0 @@ -# Глобальный объект - -Механизм работы функций и переменных в JavaScript очень отличается от большинства языков. - -Чтобы его понять, мы в этой главе рассмотрим переменные и функции в глобальной области. А в следующей -- пойдём дальше. - -[cut] - -## Глобальный объект - -*Глобальными* называют переменные и функции, которые не находятся внутри какой-то функции. То есть, иными словами, если переменная или функция не находятся внутри конструкции `function`, то они -- "глобальные". - -**В JavaScript все глобальные переменные и функции являются свойствами специального объекта, который называется *"глобальный объект"* (`global object`).** - -В браузере этот объект явно доступен под именем `window`. Объект `window` одновременно является глобальным объектом и содержит ряд свойств и методов для работы с окном браузера, но нас здесь интересует только его роль как глобального объекта. - -В других окружениях, например Node.JS, глобальный объект может быть недоступен в явном виде, но суть происходящего от этого не изменяется, поэтому далее для обозначения глобального объекта мы будем использовать `"window"`. - -**Присваивая или читая глобальную переменную, мы, фактически, работаем со свойствами `window`.** - -Например: - -```js -//+ run untrusted refresh -var a = 5; // объявление var создаёт свойство window.a -alert( window.a ); // 5 -``` - -Создать переменную можно и явным присваиванием в `window`: - -```js -//+ run untrusted refresh -window.a = 5; -alert( a ); // 5 -``` - -## Порядок инициализации - -Выполнение скрипта происходит в две фазы: -
        -
      1. На первой фазе происходит инициализация, подготовка к запуску. - -Во время инициализации скрипт сканируется на предмет объявления функций вида [Function Declaration](/function-declaration-expression), а затем -- на предмет объявления переменных `var`. Каждое такое объявление добавляется в `window`. - -**Функции, объявленные как Function Declaration, создаются сразу работающими, а переменные -- равными `undefined`.** -
      2. -
      3. На второй фазе -- собственно, выполнение. - -Присваивание (`=`) значений переменных происходит, когда поток выполнения доходит до соответствующей строчки кода, до этого они `undefined`. -
      4. -
      - -В коде ниже указано содержание глобального объекта на момент инициализации и далее последовательно по коду: - -```js -// На момент инициализации, до выполнения кода: -// window = { f: function, a: undefined, g: undefined } - -var a = 5; -// window = { f: function, *!*a: 5*/!*, g: undefined } - -function f(arg) { /*...*/ } -// window = { f: function, a: 5, g: undefined } без изменений, f обработана ранее - -var g = function(arg) { /*...*/ }; -// window = { f: function, a: 5, g: *!*function*/!* } -``` - -Кстати, тот факт, что к началу выполнения кода переменные и функции *уже* содержатся в `window`, можно легко проверить, выведя их: - -```js -//+ run untrusted refresh - -alert("a" in window); // *!*true*/!*, т.к. есть свойство window.a -alert(a); // равно *!*undefined*/!*, присваивание будет выполнено далее -alert(f); // *!*function ...*/!*, готовая к выполнению функция -alert(g); // *!*undefined*/!*, т.к. это переменная, а не Function Declaration - -var a = 5; -function f() { /*...*/ } -var g = function() { /*...*/ }; -``` - -[smart header="Присвоение переменной без объявления"] -В старом стандарте JavaScript переменную можно было создать и без объявления `var`: - -```js -//+ run -a = 5; - -alert( a ); // 5 -``` - -Такое присвоение, как и `var a = 5`, создает свойство `window.a = 5`. Отличие от `var a = 5` -- в том, что переменная будет создана не на этапе входа в область видимости, а в момент присвоения. - -Сравните два кода ниже. - -Первый выведет `undefined`, так как переменная была добавлена в `window` на фазе инициализации: - -```js -//+ run untrusted refresh -*!* -alert( a ); // undefined -*/!* - -var a = 5; -``` - -Второй код выведет ошибку, так как переменной ещё не существует: - -```js -//+ run untrusted refresh -*!* -alert( a ); // error, a is not defined -*/!* - -a = 5; -``` -Это, конечно, для общего понимания, мы всегда объявляем переменные через `var`. -[/smart] - -[smart header="Конструкции `for, if...` не влияют на видимость переменных"] -Фигурные скобки, которые используются в `for, while, if`, в отличие от объявлений функции, имеют "декоративный" характер. - -В JavaScript нет разницы между объявлением вне блока: - -```js -*!*var*/!* i; -{ - i = 5; -} -``` - -...И внутри него: - -```js -i = 5; -{ - *!*var*/!* i; -} -``` - -**Также нет разницы между объявлением в цикле и вне его:** - -```js -//+ run untrusted refresh -for (*!*var*/!* i = 0; i < 5; i++) { } -``` - -Идентичный по функциональности код: - -```js -//+ run untrusted refresh -*!*var i;*/!* -for (i = 0; i < 5; i++) { } -``` - -В обоих случаях переменная будет создана до выполнения цикла, на стадии инициализации, и ее значение будет сохранено после окончания цикла. - -[/smart] - -[smart header="Не важно, где и сколько раз объявлена переменная"] - -Объявлений `var` может быть сколько угодно: - -```js -var i = 10; - -for (var i = 0; i < 20; i++) { - ... -} - -var i = 5; -``` - -**Все `var` будут обработаны один раз, на фазе инициализации.** - -На фазе исполнения объявления `var` будут проигнорированы: они уже были обработаны. Зато будут выполнены присваивания. -[/smart] - - -[warn header="Ошибки при работе с `window` в IE8-"] - -В старых IE есть две забавные ошибки при работе с переменными в `window`: - -
        -
      1. Переопределение переменной, у которой такое же имя, как и `id` элемента, приведет к ошибке: - -```html - -
        ...
        - -``` - -А если сделать через `var`, то всё будет хорошо. - -Это была реклама того, что надо везде ставить `var`. - -
      2. -
      3. Ошибка при рекурсии через функцию-свойство `window`. Следующий код "умрет" в IE8-: - -```html - - -``` - -Проблема здесь возникает из-за того, что функция напрямую присвоена в `window.recurse = ...`. Ее не будет при обычном объявлении функции. - -**Этот пример выдаст ошибку только в настоящем IE8!** Не IE9 в режиме эмуляции. Вообще, режим эмуляции позволяет отлавливать где-то 95% несовместимостей и проблем, а для оставшихся 5% вам нужен будет настоящий IE8 в виртуальной машине. -
      4. -
      -[/warn] - - -## Итого - -В результате инициализации, к началу выполнения кода: -
        -
      1. Функции, объявленные как `Function Declaration`, создаются полностью и готовы к использованию.
      2. -
      3. Переменные объявлены, но равны `undefined`. Присваивания выполнятся позже, когда выполнение дойдет до них.
      4. -
      - - diff --git a/1-js/5-functions-closures/2-closures/1-say-phrase-first/solution.md b/1-js/5-functions-closures/2-closures/1-say-phrase-first/solution.md deleted file mode 100644 index ab6567e4..00000000 --- a/1-js/5-functions-closures/2-closures/1-say-phrase-first/solution.md +++ /dev/null @@ -1,16 +0,0 @@ -Ошибки не будет, выведет `"Вася, undefined"`. - -```js -//+ run -*!* -say('Вася'); // Что выведет? Не будет ли ошибки? -*/!* - -var phrase = 'Привет'; - -function say(name) { - alert( name + ", " + phrase ); -} -``` - -Переменная как таковая существует, вот только на момент запуска функции она равна `undefined`. \ No newline at end of file diff --git a/1-js/5-functions-closures/2-closures/1-say-phrase-first/task.md b/1-js/5-functions-closures/2-closures/1-say-phrase-first/task.md deleted file mode 100644 index fe548c0c..00000000 --- a/1-js/5-functions-closures/2-closures/1-say-phrase-first/task.md +++ /dev/null @@ -1,18 +0,0 @@ -# Что выведет say в начале кода? - -[importance 5] - -Что будет, если вызов `sayHi('Вася');` стоит в самом-самом начале, в первой строке кода? - -```js -*!* -say('Вася'); // Что выведет? Не будет ли ошибки? -*/!* - -var phrase = 'Привет'; - -function say(name) { - alert( name + ", " + phrase ); -} -``` - diff --git a/1-js/5-functions-closures/2-closures/2-which-value-is-modified/solution.md b/1-js/5-functions-closures/2-closures/2-which-value-is-modified/solution.md deleted file mode 100644 index eed571f3..00000000 --- a/1-js/5-functions-closures/2-closures/2-which-value-is-modified/solution.md +++ /dev/null @@ -1,9 +0,0 @@ -**Результатом будет `true`**, т.к. `var` обработается и переменная будет создана до выполнения кода. - -Соответственно, присвоение `value=true` сработает на локальной переменной, и `alert` выведет `true`. - -**Внешняя переменная не изменится.** - -P.S. Если `var` нет, то в функции переменная не будет найдена. Интерпретатор обратится за ней в `window` и изменит её там. - -**Так что без `var` результат будет также `true`, но внешняя переменная изменится.** diff --git a/1-js/5-functions-closures/2-closures/2-which-value-is-modified/task.md b/1-js/5-functions-closures/2-closures/2-which-value-is-modified/task.md deleted file mode 100644 index b9f72a30..00000000 --- a/1-js/5-functions-closures/2-closures/2-which-value-is-modified/task.md +++ /dev/null @@ -1,25 +0,0 @@ -# В какую переменную будет присвоено значение? - -[importance 5] - -Каков будет результат выполнения этого кода? - -```js -var value = 0; - -function f() { - if (1) { - value = true; - } else { - var value = false; - } - - alert( value ); -} - -f(); -``` - -Изменится ли внешняя переменная `value` ? - -P.S. Какими будут ответы, если из строки `var value = false` убрать `var`? \ No newline at end of file diff --git a/1-js/5-functions-closures/2-closures/3-var-window/solution.md b/1-js/5-functions-closures/2-closures/3-var-window/solution.md deleted file mode 100644 index e1609417..00000000 --- a/1-js/5-functions-closures/2-closures/3-var-window/solution.md +++ /dev/null @@ -1,29 +0,0 @@ -Результатом будет `undefined`, затем `5`. - -```js -//+ run -function test() { - - alert( window ); - - var window = 5; - - alert( window ); -} - -test(); -``` - -Такой результат получился потому, что `window` -- это глобальная переменная, но ничто не мешает объявить такую же локальную. - -Директива `var window` обработается до начала выполнения кода функции и будет создана локальная переменная, т.е. свойство `LexicalEnvironment.window`: - -```js -LexicalEnvironment = { - window: undefined -} -``` - -Когда выполнение кода начнется и сработает `alert`, он выведет уже локальную переменную, которая на тот момент равна `undefined`. - -Затем сработает присваивание, и второй `alert` выведет уже `5`. \ No newline at end of file diff --git a/1-js/5-functions-closures/2-closures/3-var-window/task.md b/1-js/5-functions-closures/2-closures/3-var-window/task.md deleted file mode 100644 index 8c0e1c7d..00000000 --- a/1-js/5-functions-closures/2-closures/3-var-window/task.md +++ /dev/null @@ -1,19 +0,0 @@ -# var window - -[importance 5] - -Каков будет результат выполнения этого кода? Почему? - -```js -function test() { - - alert( window ); - - var window = 5; - - alert( window ); -} - -test(); -``` - diff --git a/1-js/5-functions-closures/2-closures/4-call-inplace/solution.md b/1-js/5-functions-closures/2-closures/4-call-inplace/solution.md deleted file mode 100644 index 4f08292e..00000000 --- a/1-js/5-functions-closures/2-closures/4-call-inplace/solution.md +++ /dev/null @@ -1,37 +0,0 @@ -Результат - **ошибка**. Попробуйте: - -```js -//+ run no-beautify -var a = 5 - -(function() { - alert(a) -})() -``` - -Дело в том, что после `var a = 5` нет точки с запятой. - -JavaScript воспринимает этот код как если бы перевода строки не было: - -```js -//+ run no-beautify -var a = 5(function() { - alert(a) -})() -``` - -То есть, он пытается вызвать *функцию* `5`, что и приводит к ошибке. - -Если точку с запятой поставить, все будет хорошо: - -```js -//+ run no-beautify -var a = 5; - -(function() { - alert(a) -})() -``` - -Это один из наиболее частых и опасных подводных камней, приводящих к ошибкам тех, кто *не* ставит точки с запятой. - diff --git a/1-js/5-functions-closures/2-closures/4-call-inplace/task.md b/1-js/5-functions-closures/2-closures/4-call-inplace/task.md deleted file mode 100644 index aa4c1b4a..00000000 --- a/1-js/5-functions-closures/2-closures/4-call-inplace/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# Вызов "на месте" - -[importance 4] - -Каков будет результат выполнения кода? Почему? - -```js -//+ no-beautify -var a = 5 - -(function() { - alert(a) -})() -``` - -P.S. *Подумайте хорошо! Здесь все ошибаются!* -P.P.S. *Внимание, здесь подводный камень! Ок, вы предупреждены.* diff --git a/1-js/5-functions-closures/2-closures/5-access-outer-variable/solution.md b/1-js/5-functions-closures/2-closures/5-access-outer-variable/solution.md deleted file mode 100644 index 4e0a619e..00000000 --- a/1-js/5-functions-closures/2-closures/5-access-outer-variable/solution.md +++ /dev/null @@ -1,3 +0,0 @@ -Нет, нельзя. - -Локальная переменная полностью перекрывает внешнюю. diff --git a/1-js/5-functions-closures/2-closures/5-access-outer-variable/task.md b/1-js/5-functions-closures/2-closures/5-access-outer-variable/task.md deleted file mode 100644 index 13e61904..00000000 --- a/1-js/5-functions-closures/2-closures/5-access-outer-variable/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# Перекрытие переменной - -[importance 4] - -Если во внутренней функции есть своя переменная с именем `currentCount` -- можно ли в ней получить `currentCount` из внешней функции? - -```js -function makeCounter() { - var currentCount = 1; - - return function() { - var currentCount; - // можно ли здесь вывести currentCount из внешней функции (равный 1)? - }; -} -``` - diff --git a/1-js/5-functions-closures/2-closures/6-counter-window-variable/solution.md b/1-js/5-functions-closures/2-closures/6-counter-window-variable/solution.md deleted file mode 100644 index b9b3771c..00000000 --- a/1-js/5-functions-closures/2-closures/6-counter-window-variable/solution.md +++ /dev/null @@ -1,30 +0,0 @@ -Выведут **1,2,3,4.** - -Здесь внутренняя функция будет искать -- и находить `currentCount` каждый раз в самом внешнем объекте переменных: глобальном объекте `window`. - -В результате все счётчики будут разделять единое, глобальное текущее значение. - -```js -//+ run -var currentCount = 1; - -function makeCounter() { - return function() { - return currentCount++; - }; -} - -var counter = makeCounter(); -var counter2 = makeCounter(); - -*!* -alert( counter() ); // ? -alert( counter() ); // ? -*/!* - -*!* -alert( counter2() ); // ? -alert( counter2() ); // ? -*/!* -``` - diff --git a/1-js/5-functions-closures/2-closures/6-counter-window-variable/task.md b/1-js/5-functions-closures/2-closures/6-counter-window-variable/task.md deleted file mode 100644 index 48068df9..00000000 --- a/1-js/5-functions-closures/2-closures/6-counter-window-variable/task.md +++ /dev/null @@ -1,29 +0,0 @@ -# Глобальный счётчик - -[importance 5] - -Что выведут эти вызовы, если переменная `currentCount` находится вне `makeCounter`? - -```js -var currentCount = 1; - -function makeCounter() { - return function() { - return currentCount++; - }; -} - -var counter = makeCounter(); -var counter2 = makeCounter(); - -*!* -alert( counter() ); // ? -alert( counter() ); // ? -*/!* - -*!* -alert( counter2() ); // ? -alert( counter2() ); // ? -*/!* -``` - diff --git a/1-js/5-functions-closures/2-closures/6@2x.png b/1-js/5-functions-closures/2-closures/6@2x.png deleted file mode 100755 index 7a276a33..00000000 Binary files a/1-js/5-functions-closures/2-closures/6@2x.png and /dev/null differ diff --git a/1-js/5-functions-closures/2-closures/article.md b/1-js/5-functions-closures/2-closures/article.md deleted file mode 100644 index d4301d75..00000000 --- a/1-js/5-functions-closures/2-closures/article.md +++ /dev/null @@ -1,411 +0,0 @@ -# Замыкания, функции изнутри - -В этой главе мы продолжим рассматривать, как работают переменные, и, как следствие, познакомимся с замыканиями. От глобального объекта мы переходим к работе внутри функций. -[cut] -## Лексическое окружение - -Все переменные внутри функции -- это свойства специального внутреннего объекта `LexicalEnvironment`, который создаётся при её запуске. - -Мы будем называть этот объект "лексическое окружение" или просто "объект переменных". - -При запуске функция создает объект `LexicalEnvironment`, записывает туда аргументы, функции и переменные. Процесс инициализации выполняется в том же порядке, что и для глобального объекта, который, вообще говоря, является частным случаем лексического окружения. - -В отличие от `window`, объект `LexicalEnvironment` является внутренним, он скрыт от прямого доступа. - -### Пример - -Посмотрим пример, чтобы лучше понимать, как это работает: - -```js -function sayHi(name) { - var phrase = "Привет, " + name; - alert( phrase ); -} - -sayHi('Вася'); -``` - -При вызове функции: -
        -
      1. До выполнения первой строчки её кода, на стадии инициализации, интерпретатор создает пустой объект `LexicalEnvironment` и заполняет его. - -В данном случае туда попадает аргумент `name` и единственная переменная `phrase`: - -```js -function sayHi(name) { -*!* - // LexicalEnvironment = { name: 'Вася', phrase: undefined } -*/!* - var phrase = "Привет, " + name; - alert( phrase ); -} - -sayHi('Вася'); -``` - -
      2. -
      3. Функция выполняется. - -Во время выполнения происходит присвоение локальной переменной `phrase`, то есть, другими словами, присвоение свойству `LexicalEnvironment.phrase` нового значения: - -```js -function sayHi(name) { - // LexicalEnvironment = { name: 'Вася', phrase: undefined } - var phrase = "Привет, " + name; - -*!* - // LexicalEnvironment = { name: 'Вася', phrase: 'Привет, Вася'} -*/!* - alert( phrase ); -} - -sayHi('Вася'); -``` - -
      4. -
      5. В конце выполнения функции объект с переменными обычно выбрасывается и память очищается. В примерах выше так и происходит. Через некоторое время мы рассмотрим более сложные ситуации, при которых объект с переменными сохраняется и после завершения функции.
      6. -
      - -[smart header="Тонкости спецификации"] -Если почитать спецификацию ECMA-262, то мы увидим, что речь идёт о двух объектах: `VariableEnvironment` и `LexicalEnvironment`. - -Но там же замечено, что в реализациях эти два объекта могут быть объединены. Так что мы избегаем лишних деталей и используем везде термин `LexicalEnvironment`, это достаточно точно позволяет описать происходящее. - -Более формальное описание находится в спецификации ECMA-262, секции 10.2-10.5 и 13. -[/smart] - - -## Доступ ко внешним переменным - -Из функции мы можем обратиться не только к локальной переменной, но и к внешней: - -```js -var userName = "Вася"; - -function sayHi() { - alert( userName ); // "Вася" -} -``` - -**Интерпретатор, при доступе к переменной, сначала пытается найти переменную в текущем `LexicalEnvironment`, а затем, если её нет -- ищет во внешнем объекте переменных. В данном случае им является `window`.** - -Такой порядок поиска возможен благодаря тому, что ссылка на внешний объект переменных хранится в специальном внутреннем свойстве функции, которое называется `[[Scope]]`. Это свойство закрыто от прямого доступа, но знание о нём очень важно для понимания того, как работает JavaScript. - -**При создании функция получает скрытое свойство `[[Scope]]`, которое ссылается на лексическое окружение, в котором она была создана.** - -В примере выше таким окружением является `window`, так что создаётся свойство: -```js -//+ no-beautify -sayHi.[[Scope]] = window -``` - -Это свойство никогда не меняется. Оно всюду следует за функцией, привязывая её, таким образом, к месту своего рождения. - -При запуске функции её объект переменных `LexicalEnvironment` получает ссылку на "внешнее лексическое окружение" со значением из `[[Scope]]`. - -Если переменная не найдена в функции -- она будет искаться снаружи. - -Именно благодаря этой механике в примере выше `alert(userName)` выводит внешнюю переменную. На уровне кода это выглядит как поиск во внешней области видимости, вне функции. - -Если обобщить: -
        -
      • Каждая функция при создании получает ссылку `[[Scope]]` на объект с переменными, в контексте которого была создана.
      • -
      • При запуске функции создаётся новый объект с переменными `LexicalEnvironment`. Он получает ссылку на внешний объект переменных из `[[Scope]]`.
      • -
      • При поиске переменных он осуществляется сначала в текущем объекте переменных, а потом -- по этой ссылке.
      • -
      - -Выглядит настолько просто, что непонятно -- зачем вообще говорить об этом `[[Scope]]`, об объектах переменных. Сказали бы: "Функция читает переменные снаружи" -- и всё. Но знание этих деталей позволит нам легко объяснить и понять более сложные ситуации, с которыми мы столкнёмся далее. - -## Всегда текущее значение - -Значение переменной из внешней области берётся всегда текущее. Оно может быть уже не то, что было на момент создания функции. - -Например, в коде ниже функция `sayHi` берёт `phrase` из внешней области: - -```js -//+ run no-beautify - -var phrase = 'Привет'; - -function say(name) { - alert(phrase + ', ' + name); -} - -*!* -say('Вася'); // Привет, Вася (*) -*/!* - -phrase = 'Пока'; - -*!* -say('Вася'); // Пока, Вася (**) -*/!* -``` - -На момент первого запуска `(*)`, переменная `phrase` имела значение `'Привет'`, а ко второму `(**)` изменила его на `'Пока'`. - -Это естественно, ведь для доступа к внешней переменной функция по ссылке `[[Scope]]` обращается во внешний объект переменных и берёт то значение, которое там есть на момент обращения. - -## Вложенные функции - -Внутри функции можно объявлять не только локальные переменные, но и другие функции. - -К примеру, вложенная функция может помочь лучше организовать код: - -```js -//+ run -function sayHiBye(firstName, lastName) { - - alert( "Привет, " + getFullName() ); - alert( "Пока, " + getFullName() ); - -*!* - function getFullName() { - return firstName + " " + lastName; - } -*/!* - -} - -sayHiBye("Вася", "Пупкин"); // Привет, Вася Пупкин ; Пока, Вася Пупкин -``` - -Здесь, для удобства, создана вспомогательная функция `getFullName()`. - -Вложенные функции получают `[[Scope]]` так же, как и глобальные. В нашем случае: - -```js -//+ no-beautify -getFullName.[[Scope]] = объект переменных текущего запуска sayHiBye -``` - -Благодаря этому `getFullName()` получает снаружи `firstName` и `lastName`. - -Заметим, что если переменная не найдена во внешнем объекте переменных, то она ищется в ещё более внешнем (через `[[Scope]]` внешней функции), то есть, такой пример тоже будет работать: - -```js -//+ run -var phrase = 'Привет'; - -function say() { - - function go() { - alert( phrase ); // найдёт переменную снаружи - } - - go(); -} -``` - -## Возврат функции - -Рассмотрим более "продвинутый" вариант, при котором внутри одной функции создаётся другая и возвращается в качестве результата. - -В разработке интерфейсов это совершенно стандартный приём, функция затем может назначаться как обработчик действий посетителя. - -Здесь мы будем создавать функцию-счётчик, которая считает свои вызовы и возвращает их текущее число. - -В примере ниже `makeCounter` создает такую функцию: - -```js -//+ run -function makeCounter() { -*!* - var currentCount = 1; -*/!* - - return function() { // (**) - return currentCount++; - }; -} - -var counter = makeCounter(); // (*) - -// каждый вызов увеличивает счётчик и возвращает результат -alert( counter() ); // 1 -alert( counter() ); // 2 -alert( counter() ); // 3 - -// создать другой счётчик, он будет независим от первого -var counter2 = makeCounter(); -alert( counter2() ); // 1 -``` - -Как видно, мы получили два независимых счётчика `counter` и `counter2`, каждый из которых незаметным снаружи образом сохраняет текущее количество вызовов. - -Где? Конечно, во внешней переменной `currentCount`, которая у каждого счётчика своя. - -Если подробнее описать происходящее: - -
        -
      1. В строке `(*)` запускается `makeCounter()`. При этом создаётся `LexicalEnvironment` для переменных текущего вызова. В функции есть одна переменная `var currentCount`, которая станет свойством этого объекта. Она изначально инициализуется в `undefined`, затем, в процессе выполнения, получит значение `1`: - -```js -function makeCounter() { -*!* - // LexicalEnvironment = { currentCount: undefined } -*/!* - - var currentCount = 1; - -*!* - // LexicalEnvironment = { currentCount: 1 } -*/!* - - return function() { // [[Scope]] -> LexicalEnvironment (**) - return currentCount++; - }; -} - -var counter = makeCounter(); // (*) -``` - -
      2. -
      3. В процессе выполнения `makeCounter()` создаёт функцию в строке `(**)`. При создании эта функция получает внутреннее свойство `[[Scope]]` со ссылкой на текущий `LexicalEnvironment`.
      4. -
      5. Далее вызов `makeCounter()` завершается и функция `(**)` возвращается и сохраняется во внешней переменной `counter` `(*)`.
      6. -
      - -На этом создание "счётчика" завершено. - -Итоговым значением, записанным в переменную `counter`, является функция: - -```js -function() { // [[Scope]] -> {currentCount: 1} - return currentCount++; -}; -``` - -Возвращённая из `makeCounter()` функция `counter` помнит (через `[[Scope]]`) о том, в каком окружении была создана. - -Это и используется для хранения текущего значения счётчика. - -Далее, когда-нибудь, функция `counter` будет вызвана. Мы не знаем, когда это произойдёт. Может быть, прямо сейчас, но, вообще говоря, совсем не факт. - -Эта функция состоит из одной строки: `return currentCount++`, ни переменных ни параметров в ней нет, поэтому её собственный объект переменных, для краткости назовём его `LE` -- будет пуст. - -Однако, у неё есть свойство `[[Scope]]`, которое указывает на внешнее окружение. Чтобы увеличить и вернуть `currentCount`, интерпретатор ищет в текущем объекте переменных `LE`, не находит, затем идёт во внешний объект, там находит, изменяет и возвращает новое значение: - -```js -//+ run -function makeCounter() { - var currentCount = 1; - - return function() { - return currentCount++; - }; -} - -var counter = makeCounter(); // [[Scope]] -> {currentCount: 1} - -alert( counter() ); // 1, [[Scope]] -> {currentCount: 1} -alert( counter() ); // 2, [[Scope]] -> {currentCount: 2} -alert( counter() ); // 3, [[Scope]] -> {currentCount: 3} -``` - -**Переменную во внешней области видимости можно не только читать, но и изменять.** - - -В примере выше было создано несколько счётчиков. Все они взаимно независимы: - -```js -var counter = makeCounter(); - -var counter2 = makeCounter(); - -alert( counter() ); // 1 -alert( counter() ); // 2 -alert( counter() ); // 3 - -alert( counter2() ); // 1, *!*счётчики независимы*/!* -``` - -Они независимы, потому что при каждом запуске `makeCounter` создаётся свой объект переменных `LexicalEnvironment`, со своим свойством `currentCount`, на который новый счётчик получит ссылку `[[Scope]]`. - - -## Свойства функции - -Функция в JavaScript является объектом, поэтому можно присваивать свойства прямо к ней, вот так: - -```js -//+ run -function f() {} - -f.test = 5; -alert( f.test ); -``` - -Свойства функции не стоит путать с переменными и параметрами. Они совершенно никак не связаны. Переменные доступны только внутри функции, они создаются в процессе её выполнения. Это -- использование функции "как функции". - -А свойство у функции -- доступно отовсюду и всегда. Это -- использование функции "как объекта". - -Если хочется привязать значение к функции, то можно им воспользоваться вместо внешних переменных. - -В качестве демонстрации, перепишем пример со счётчиком: - -```js -//+ run -function makeCounter() { -*!* - function counter() { - return counter.currentCount++; - }; - counter.currentCount = 1; -*/!* - - return counter; -} - -var counter = makeCounter(); -alert( counter() ); // 1 -alert( counter() ); // 2 -``` - -При запуске пример работает также. - -Принципиальная разница -- во внутренней механике и в том, что свойство функции, в отличие от переменной из замыкания -- общедоступно, к нему имеет доступ любой, у кого есть объект функции. - -Например, можно взять и поменять счётчик из внешнего кода: - -```js -var counter = makeCounter(); -alert( counter() ); // 1 - -*!* -counter.currentCount = 5; -*/!* - -alert( counter() ); // 5 -``` - -[smart header="Статические переменные"] -Иногда свойства, привязанные к функции, называют "статическими переменными". - -В некоторых языках программирования можно объявлять переменную, которая сохраняет значение между вызовами функции. В JavaScript ближайший аналог -- такое вот свойство функции. -[/smart] - - -## Итого: замыкания - -[Замыкание](http://en.wikipedia.org/wiki/Closure_(computer_science)) -- это функция вместе со всеми внешними переменными, которые ей доступны. - -Таково стандартное определение, которое есть в Wikipedia и большинстве серьёзных источников по программированию. То есть, замыкание -- это функция + внешние переменные. - -Тем не менее, в JavaScript есть небольшая терминологическая особенность. - -**Обычно, говоря "замыкание функции", подразумевают не саму эту функцию, а именно внешние переменные.** - -Иногда говорят "переменная берётся из замыкания". Это означает -- из внешнего объекта переменных. - - -[smart header="Что это такое -- \"понимать замыкания?\""] -Иногда говорят "Вася молодец, понимает замыкания!". Что это такое -- "понимать замыкания", какой смысл обычно вкладывают в эти слова? - -"Понимать замыкания" в JavaScript означает понимать следующие вещи: -
        -
      1. Все переменные и параметры функций являются свойствами объекта переменных `LexicalEnvironment`. Каждый запуск функции создает новый такой объект. На верхнем уровне им является "глобальный объект", в браузере -- `window`.
      2. -
      3. При создании функция получает системное свойство `[[Scope]]`, которое ссылается на `LexicalEnvironment`, в котором она была создана.
      4. -
      5. При вызове функции, куда бы её ни передали в коде -- она будет искать переменные сначала у себя, а затем во внешних `LexicalEnvironment` с места своего "рождения".
      6. -
      - -В следующих главах мы углубим это понимание дополнительными примерами, а также рассмотрим, что происходит с памятью. -[/smart] diff --git a/1-js/5-functions-closures/3-scope-new-function/article.md b/1-js/5-functions-closures/3-scope-new-function/article.md deleted file mode 100644 index 51cebe40..00000000 --- a/1-js/5-functions-closures/3-scope-new-function/article.md +++ /dev/null @@ -1,93 +0,0 @@ -# [[Scope]] для new Function - - -## Присвоение [[Scope]] для new Function [#scope-Function] - -Есть одно исключение из общего правила присвоения `[[Scope]]`, которое мы рассматривали в предыдущей главе. - -При создании функции с использованием `new Function`, её свойство `[[Scope]]` ссылается не на текущий `LexicalEnvironment`, а на `window`. - -## Пример - -Следующий пример демонстрирует как функция, созданная `new Function`, игнорирует внешнюю переменную `a` и выводит глобальную вместо неё: - -```js -//+ run untrusted refresh -var a = 1; - -function getFunc() { - var a = 2; - -*!* - var func = new Function('', 'alert(a)'); -*/!* - - return func; -} - -getFunc()(); // *!*1*/!*, из window -``` - -Сравним с обычным поведением: - -```js -//+ run untrusted refresh -var a = 1; - -function getFunc() { - var a = 2; - -*!* - var func = function() { alert(a); }; -*/!* - - return func; -} - -getFunc()(); // *!*2*/!*, из LexicalEnvironment функции getFunc -``` - - -## Почему так сделано? - -[warn header="Продвинутые знания"] -Содержимое этой секции содержит продвинутую информацию теоретического характера, которая прямо сейчас не обязательна для дальнейшего изучения JavaScript. -[/warn] - -Эта особенность `new Function`, хоть и выглядит странно, на самом деле весьма полезна. - -Представьте себе, что нам действительно нужно создать функцию из строки кода. Текст кода этой функции неизвестен на момент написания скрипта (иначе зачем `new Function`), но станет известен позже, например получен с сервера или из других источников данных. - -Предположим, что этому коду надо будет взаимодействовать с внешними переменными основного скрипта. - -Но проблема в том, что JavaScript при выкладывании на "боевой сервер" предварительно сжимается минификатором -- специальной программой, которая уменьшает размер кода, убирая из него лишние комментарии, пробелы, что очень важно -- переименовывает локальные переменные на более короткие. - -То есть, если внутри функции есть `var userName`, то минификатор заменит её на `var a` (или другую букву, чтобы не было конфликта), предполагая, что так как переменная видна только внутри функции, то этого всё равно никто не заметит, а код станет короче. И обычно проблем нет. - -...Но если бы `new Function` могла обращаться к внешним переменным, то при попытке доступа к `userName` в сжатом коде была бы ошибка, так как минификатор переименовал её. - -**Получается, что даже если бы мы захотели использовать локальные переменные в `new Function`, то после сжатия были бы проблемы, так как минификатор переименовывает локальные переменные.** - -Описанная особенность `new Function` просто-таки спасает нас от ошибок. - -Ну а если внутри функции, создаваемой через `new Function`, всё же нужно использовать какие-то данные -- без проблем, нужно всего лишь предусмотреть соответствующие параметры и передавать их явным образом, например так: - -```js -//+ run untrusted refresh no-beautify -*!* -var sum = new Function('a, b', ' return a + b; '); -*/!* - -var a = 1, b = 2; - -*!* -alert( sum(a, b) ); // 3 -*/!* -``` - -## Итого - -
        -
      • Функции, создаваемые через `new Function`, имеют значением `[[Scope]]` не внешний объект переменных, а `window`.
      • -
      • Следствие -- такие функции не могут использовать замыкание. Но это хорошо, так как бережёт от ошибок проектирования, да и при сжатии JavaScript проблем не будет. Если же внешние переменные реально нужны -- их можно передать в качестве параметров.
      • -
      diff --git a/1-js/5-functions-closures/4-closures-usage/1-closure-sum/solution.md b/1-js/5-functions-closures/4-closures-usage/1-closure-sum/solution.md deleted file mode 100644 index 2e28ae36..00000000 --- a/1-js/5-functions-closures/4-closures-usage/1-closure-sum/solution.md +++ /dev/null @@ -1,18 +0,0 @@ -Чтобы вторые скобки в вызове работали - первые должны возвращать функцию. - -Эта функция должна знать про `a` и уметь прибавлять `a` к `b`. Вот так: - -```js -//+ run -function sum(a) { - - return function(b) { - return a + b; // возьмет a из внешнего LexicalEnvironment - }; - -} - -alert( sum(1)(2) ); -alert( sum(5)(-1) ); -``` - diff --git a/1-js/5-functions-closures/4-closures-usage/1-closure-sum/task.md b/1-js/5-functions-closures/4-closures-usage/1-closure-sum/task.md deleted file mode 100644 index 5de93f5e..00000000 --- a/1-js/5-functions-closures/4-closures-usage/1-closure-sum/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Сумма через замыкание - -[importance 4] - -Напишите функцию `sum`, которая работает так: `sum(a)(b) = a+b`. - -Да, именно так, через двойные скобки (это не опечатка). Например: - -```js -sum(1)(2) = 3 -sum(5)(-1) = 4 -``` - diff --git a/1-js/5-functions-closures/4-closures-usage/2-stringbuffer/_js.view/solution.js b/1-js/5-functions-closures/4-closures-usage/2-stringbuffer/_js.view/solution.js deleted file mode 100644 index bda57778..00000000 --- a/1-js/5-functions-closures/4-closures-usage/2-stringbuffer/_js.view/solution.js +++ /dev/null @@ -1,10 +0,0 @@ -function makeBuffer() { - var text = ''; - - return function(piece) { - if (arguments.length == 0) { // вызов без аргументов - return text; - } - text += piece; - }; -}; \ No newline at end of file diff --git a/1-js/5-functions-closures/4-closures-usage/2-stringbuffer/_js.view/test.js b/1-js/5-functions-closures/4-closures-usage/2-stringbuffer/_js.view/test.js deleted file mode 100644 index df3c04f9..00000000 --- a/1-js/5-functions-closures/4-closures-usage/2-stringbuffer/_js.view/test.js +++ /dev/null @@ -1,21 +0,0 @@ -var buffer; -beforeEach(function() { - buffer = makeBuffer(); -}); - -it("возвращает пустую строку по умолчанию", function() { - assert.strictEqual(buffer(), ""); -}); - -it("добавляет аргументы в буффер", function() { - buffer('Замыкания'); - buffer(' Использовать'); - buffer(' Нужно!'); - assert.equal(buffer(), 'Замыкания Использовать Нужно!'); -}); - -it("приводит всё к строке", function() { - buffer(null); - buffer(false); - assert.equal(buffer(), "nullfalse"); -}); \ No newline at end of file diff --git a/1-js/5-functions-closures/4-closures-usage/2-stringbuffer/solution.md b/1-js/5-functions-closures/4-closures-usage/2-stringbuffer/solution.md deleted file mode 100644 index 5f946bfb..00000000 --- a/1-js/5-functions-closures/4-closures-usage/2-stringbuffer/solution.md +++ /dev/null @@ -1,32 +0,0 @@ -Текущее значение текста удобно хранить в замыкании, в локальной переменной `makeBuffer`: - -```js -//+ run -function makeBuffer() { - var text = ''; - - return function(piece) { - if (arguments.length == 0) { // вызов без аргументов - return text; - } - text += piece; - }; -}; - -var buffer = makeBuffer(); - -// добавить значения к буферу -buffer('Замыкания'); -buffer(' Использовать'); -buffer(' Нужно!'); -alert( buffer() ); // 'Замыкания Использовать Нужно!' - -var buffer2 = makeBuffer(); -buffer2(0); -buffer2(1); -buffer2(0); - -alert( buffer2() ); // '010' -``` - -Начальное значение `text = ''` -- пустая строка. Поэтому операция `text += piece` прибавляет `piece` к строке, автоматически преобразуя его к строковому типу, как и требовалось в условии. diff --git a/1-js/5-functions-closures/4-closures-usage/2-stringbuffer/task.md b/1-js/5-functions-closures/4-closures-usage/2-stringbuffer/task.md deleted file mode 100644 index 9f28b7cd..00000000 --- a/1-js/5-functions-closures/4-closures-usage/2-stringbuffer/task.md +++ /dev/null @@ -1,45 +0,0 @@ -# Функция - строковый буфер - -[importance 5] - -В некоторых языках программирования существует объект "строковый буфер", который аккумулирует внутри себя значения. Его функционал состоит из двух возможностей: -
        -
      1. Добавить значение в буфер.
      2. -
      3. Получить текущее содержимое.
      4. -
      - -**Задача -- реализовать строковый буфер на функциях в JavaScript, со следующим синтаксисом:** - -
        -
      • Создание объекта: `var buffer = makeBuffer();`.
      • -
      • Вызов `makeBuffer` должен возвращать такую функцию `buffer`, которая при вызове `buffer(value)` добавляет значение в некоторое внутреннее хранилище, а при вызове без аргументов `buffer()` -- возвращает его.
      • -
      - -Вот пример работы: - -```js -function makeBuffer() { /* ваш код */ } - -var buffer = makeBuffer(); - -// добавить значения к буферу -buffer('Замыкания'); -buffer(' Использовать'); -buffer(' Нужно!'); - -// получить текущее значение -alert( buffer() ); // Замыкания Использовать Нужно! -``` - -Буфер должен преобразовывать все данные к строковому типу: - -```js -var buffer = makeBuffer(); -buffer(0); -buffer(1); -buffer(0); - -alert( buffer() ); // '010' -``` - -Решение не должно использовать глобальные переменные. diff --git a/1-js/5-functions-closures/4-closures-usage/3-stringbuffer-with-clear/_js.view/solution.js b/1-js/5-functions-closures/4-closures-usage/3-stringbuffer-with-clear/_js.view/solution.js deleted file mode 100644 index b7fbc6f6..00000000 --- a/1-js/5-functions-closures/4-closures-usage/3-stringbuffer-with-clear/_js.view/solution.js +++ /dev/null @@ -1,16 +0,0 @@ -function makeBuffer() { - var text = ''; - - function buffer(piece) { - if (arguments.length == 0) { // вызов без аргументов - return text; - } - text += piece; - }; - - buffer.clear = function() { - text = ""; - } - - return buffer; -}; \ No newline at end of file diff --git a/1-js/5-functions-closures/4-closures-usage/3-stringbuffer-with-clear/_js.view/test.js b/1-js/5-functions-closures/4-closures-usage/3-stringbuffer-with-clear/_js.view/test.js deleted file mode 100644 index 0543cc90..00000000 --- a/1-js/5-functions-closures/4-closures-usage/3-stringbuffer-with-clear/_js.view/test.js +++ /dev/null @@ -1,30 +0,0 @@ -var buffer; - -beforeEach(function() { - buffer = makeBuffer(); -}); - -it("возвращает пустую строку по умолчанию", function() { - assert.strictEqual(buffer(), ""); -}); - -it("добавляет аргументы в буффер", function() { - buffer('Замыкания'); - buffer(' Использовать'); - buffer(' Нужно!'); - assert.equal(buffer(), 'Замыкания Использовать Нужно!'); -}); - -it("приводит всё к строке", function() { - buffer(null); - buffer(false); - assert.equal(buffer(), "nullfalse"); -}); - -it("очищает буфер вызовом clear", function() { - buffer("test"); - buffer.clear(); - buffer("первый"); - buffer("второй"); - assert.equal(buffer(), "первыйвторой"); -}); \ No newline at end of file diff --git a/1-js/5-functions-closures/4-closures-usage/3-stringbuffer-with-clear/solution.md b/1-js/5-functions-closures/4-closures-usage/3-stringbuffer-with-clear/solution.md deleted file mode 100644 index c4c45809..00000000 --- a/1-js/5-functions-closures/4-closures-usage/3-stringbuffer-with-clear/solution.md +++ /dev/null @@ -1,34 +0,0 @@ - - -```js -//+ run -function makeBuffer() { - var text = ''; - - function buffer(piece) { - if (arguments.length == 0) { // вызов без аргументов - return text; - } - text += piece; - }; - - buffer.clear = function() { - text = ""; - } - - return buffer; -}; - -var buffer = makeBuffer(); - -buffer("Тест"); -buffer(" тебя не съест "); -alert( buffer() ); // Тест тебя не съест - -*!* -buffer.clear(); -*/!* - -alert( buffer() ); // "" -``` - diff --git a/1-js/5-functions-closures/4-closures-usage/3-stringbuffer-with-clear/task.md b/1-js/5-functions-closures/4-closures-usage/3-stringbuffer-with-clear/task.md deleted file mode 100644 index 319af876..00000000 --- a/1-js/5-functions-closures/4-closures-usage/3-stringbuffer-with-clear/task.md +++ /dev/null @@ -1,24 +0,0 @@ -# Строковый буфер с очисткой - -[importance 5] - -Добавьте буферу из решения задачи [](/task/stringbuffer) метод `buffer.clear()`, который будет очищать текущее содержимое буфера: - -```js -function makeBuffer() { - ...ваш код... -} - -var buffer = makeBuffer(); - -buffer("Тест"); -buffer(" тебя не съест "); -alert( buffer() ); // Тест тебя не съест - -*!* -buffer.clear(); -*/!* - -alert( buffer() ); // "" -``` - diff --git a/1-js/5-functions-closures/4-closures-usage/4-sort-by-field/solution.md b/1-js/5-functions-closures/4-closures-usage/4-sort-by-field/solution.md deleted file mode 100644 index 43ded8f9..00000000 --- a/1-js/5-functions-closures/4-closures-usage/4-sort-by-field/solution.md +++ /dev/null @@ -1,37 +0,0 @@ - - -```js -//+ run -var users = [{ - name: "Вася", - surname: 'Иванов', - age: 20 -}, { - name: "Петя", - surname: 'Чапаев', - age: 25 -}, { - name: "Маша", - surname: 'Медведева', - age: 18 -}]; - -*!* -function byField(field) { - return function(a, b) { - return a[field] > b[field] ? 1 : -1; - } - } -*/!* - -users.sort(byField('name')); -users.forEach(function(user) { - alert( user.name ); -}); - -users.sort(byField('age')); -users.forEach(function(user) { - alert( user.name ); -}); -``` - diff --git a/1-js/5-functions-closures/4-closures-usage/4-sort-by-field/task.md b/1-js/5-functions-closures/4-closures-usage/4-sort-by-field/task.md deleted file mode 100644 index 36701c23..00000000 --- a/1-js/5-functions-closures/4-closures-usage/4-sort-by-field/task.md +++ /dev/null @@ -1,53 +0,0 @@ -# Сортировка - -[importance 5] - -У нас есть массив объектов: - -```js -var users = [{ - name: "Вася", - surname: 'Иванов', - age: 20 -}, { - name: "Петя", - surname: 'Чапаев', - age: 25 -}, { - name: "Маша", - surname: 'Медведева', - age: 18 -}]; -``` - -Обычно сортировка по нужному полю происходит так: - -```js -// по полю name (Вася, Маша, Петя) -users.sort(function(a, b) { - return a.name > b.name ? 1 : -1; -}); - -// по полю age (Маша, Вася, Петя) -users.sort(function(a, b) { - return a.age > b.age ? 1 : -1; -}); -``` - -Мы хотели бы упростить синтаксис до одной строки, вот так: - -```js -users.sort(byField('name')); -users.forEach(function(user) { - alert( user.name ); -}); // Вася, Маша, Петя - -users.sort(byField('age')); -users.forEach(function(user) { - alert( user.name ); -}); // Маша, Вася, Петя -``` - -То есть, вместо того, чтобы каждый раз писать в `sort` `function...` -- будем использовать `byField(...)` - -Напишите функцию `byField(field)`, которую можно использовать в `sort` для сравнения объектов по полю `field`, чтобы пример выше заработал. diff --git a/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/_js.view/solution.js b/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/_js.view/solution.js deleted file mode 100644 index 90cf2689..00000000 --- a/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/_js.view/solution.js +++ /dev/null @@ -1,24 +0,0 @@ -function filter(arr, func) { - var result = []; - - for (var i = 0; i < arr.length; i++) { - var val = arr[i]; - if (func(val)) { - result.push(val); - } - } - - return result; -} - -function inArray(arr) { - return function(x) { - return arr.indexOf(x) != -1; - }; -} - -function inBetween(a, b) { - return function(x) { - return x >= a && x <= b; - }; -} \ No newline at end of file diff --git a/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/_js.view/source.js b/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/_js.view/source.js deleted file mode 100644 index e512f26c..00000000 --- a/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/_js.view/source.js +++ /dev/null @@ -1,11 +0,0 @@ -function filter(arr, fuc) { - // ...ваш код... -} - -function inBetween(a, b) { - // ...ваш код... -} - -function inArray(arr) { - // ...ваш код... -} \ No newline at end of file diff --git a/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/_js.view/test.js b/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/_js.view/test.js deleted file mode 100644 index f03fb743..00000000 --- a/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/_js.view/test.js +++ /dev/null @@ -1,58 +0,0 @@ -var arr; - -before(function() { - arr = [1, 2, 3, 4, 5, 6, 7]; -}); - -describe("inArray", function() { - var checkInArr; - - before(function() { - checkInArr = inArray(arr); - }); - - it("возвращает фильтр для значений в массиве", function() { - assert.isTrue(checkInArr(5)); - assert.isFalse(checkInArr(0)); - }); -}); - - -describe("inBetween", function() { - var checkBetween36; - - before(function() { - checkBetween36 = inBetween(3, 6); - }); - - it("возвращает фильтрa для значений в промежутке", function() { - assert.isTrue(checkBetween36(5)); - assert.isFalse(checkBetween36(0)); - }); -}); - - -describe("filter", function() { - - it("фильтрует через func", function() { - assert.deepEqual(filter(arr, function(a) { - return a % 2 == 0; - }), [2, 4, 6]); - }); - - it("не модифицирует исходный массив", function() { - filter(arr, function(a) { - return a % 2 == 0; - }); - assert.deepEqual(arr, [1, 2, 3, 4, 5, 6, 7]); - }); - - it("поддерживает фильтр inBetween", function() { - assert.deepEqual(filter(arr, inBetween(3, 6)), [3, 4, 5, 6]); - }); - - it("поддерживает фильтр inArray", function() { - assert.deepEqual(filter(arr, inArray([1, 2, 3])), [1, 2, 3]); - }); - -}); \ No newline at end of file diff --git a/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/solution.md b/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/solution.md deleted file mode 100644 index 114fdd5f..00000000 --- a/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/solution.md +++ /dev/null @@ -1,82 +0,0 @@ -# Функция фильтрации - -```js -//+ run -function filter(arr, func) { - var result = []; - - for (var i = 0; i < arr.length; i++) { - var val = arr[i]; - if (func(val)) { - result.push(val); - } - } - - return result; -} - -var arr = [1, 2, 3, 4, 5, 6, 7]; - -alert(filter(arr, function(a) { - return a % 2 == 0; -})); // 2, 4, 6 -``` - -# Фильтр inBetween - -```js -//+ run -function filter(arr, func) { - var result = []; - - for (var i = 0; i < arr.length; i++) { - var val = arr[i]; - if (func(val)) { - result.push(val); - } - } - - return result; -} - -*!* -function inBetween(a, b) { - return function(x) { - return x >= a && x <= b; - }; - } -*/!* - -var arr = [1, 2, 3, 4, 5, 6, 7]; -alert( filter(arr, inBetween(3, 6)) ); // 3,4,5,6 -``` - -# Фильтр inArray - -```js -//+ run -function filter(arr, func) { - var result = []; - - for (var i = 0; i < arr.length; i++) { - var val = arr[i]; - if (func(val)) { - result.push(val); - } - } - - return result; -} - -*!* -function inArray(arr) { - return function(x) { - return arr.indexOf(x) != -1; - }; - } -*/!* - -var arr = [1, 2, 3, 4, 5, 6, 7]; -alert( filter(arr, inArray([1, 2, 10])) ); // 1,2 -``` - diff --git a/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/task.md b/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/task.md deleted file mode 100644 index 7ee691b5..00000000 --- a/1-js/5-functions-closures/4-closures-usage/5-filter-through-function/task.md +++ /dev/null @@ -1,29 +0,0 @@ -# Фильтрация через функцию - -[importance 5] - -
        -
      1. Создайте функцию `filter(arr, func)`, которая получает массив `arr` и возвращает новый, в который входят только те элементы `arr`, для которых `func` возвращает `true`.
      2. -
      3. Создайте набор "готовых фильтров": `inBetween(a,b)` -- "между a,b", `inArray([...])` -- "в массиве `[...]`". -Использование должно быть таким: -
          -
        • `filter(arr, inBetween(3,6))` -- выберет только числа от 3 до 6,
        • -
        • `filter(arr, inArray([1,2,3]))` -- выберет только элементы, совпадающие с одним из значений массива.
        • -
        -
      4. -
      -Пример, как это должно работать: - -```js -/* .. ваш код для filter, inBetween, inArray */ -var arr = [1, 2, 3, 4, 5, 6, 7]; - -alert(filter(arr, function(a) { - return a % 2 == 0 -})); // 2,4,6 - -alert( filter(arr, inBetween(3, 6)) ); // 3,4,5,6 - -alert( filter(arr, inArray([1, 2, 10])) ); // 1,2 -``` - diff --git a/1-js/5-functions-closures/4-closures-usage/6-make-army/_js.view/solution.js b/1-js/5-functions-closures/4-closures-usage/6-make-army/_js.view/solution.js deleted file mode 100644 index 2455c301..00000000 --- a/1-js/5-functions-closures/4-closures-usage/6-make-army/_js.view/solution.js +++ /dev/null @@ -1,19 +0,0 @@ -function makeArmy() { - - var shooters = []; - - for (var i = 0; i < 10; i++) { - - var shooter = (function(x) { - - return function() { - alert(x); - }; - - })(i); - - shooters.push(shooter); - } - - return shooters; -} \ No newline at end of file diff --git a/1-js/5-functions-closures/4-closures-usage/6-make-army/_js.view/source.js b/1-js/5-functions-closures/4-closures-usage/6-make-army/_js.view/source.js deleted file mode 100644 index f9058674..00000000 --- a/1-js/5-functions-closures/4-closures-usage/6-make-army/_js.view/source.js +++ /dev/null @@ -1,13 +0,0 @@ -function makeArmy() { - - var shooters = []; - - for (var i = 0; i < 10; i++) { - var shooter = function() { // функция-стрелок - alert(i); // выводит свой номер - }; - shooters.push(shooter); - } - - return shooters; -} \ No newline at end of file diff --git a/1-js/5-functions-closures/4-closures-usage/6-make-army/_js.view/test.js b/1-js/5-functions-closures/4-closures-usage/6-make-army/_js.view/test.js deleted file mode 100644 index fdfabf66..00000000 --- a/1-js/5-functions-closures/4-closures-usage/6-make-army/_js.view/test.js +++ /dev/null @@ -1,20 +0,0 @@ -var army; -before(function() { - army = makeArmy(); - window.alert = sinon.stub(window, "alert"); -}); - -it("army[0] выводит 0", function() { - army[0](); - assert(alert.calledWith(0)); -}); - - -it("army[5] функция выводит 5", function() { - army[5](); - assert(alert.calledWith(5)); -}); - -after(function() { - window.alert.restore(); -}); \ No newline at end of file diff --git a/1-js/5-functions-closures/4-closures-usage/6-make-army/solution.md b/1-js/5-functions-closures/4-closures-usage/6-make-army/solution.md deleted file mode 100644 index 5628e800..00000000 --- a/1-js/5-functions-closures/4-closures-usage/6-make-army/solution.md +++ /dev/null @@ -1,218 +0,0 @@ -# Что происходит в этом коде - -Функция `makeArmy` делает следующее: -
        -
      1. Создаёт пустой массив `shooter`: - -```js -var shooters = []; -``` - -
      2. -
      3. В цикле заполняет массив элементами через `shooter.push`. -При этом каждый элемент массива -- это функция, так что в итоге после цикла массив будет таким: - -```js -//+ no-beautify -shooters = [ - function () { alert(i); }, - function () { alert(i); }, - function () { alert(i); }, - function () { alert(i); }, - function () { alert(i); }, - function () { alert(i); }, - function () { alert(i); }, - function () { alert(i); }, - function () { alert(i); }, - function () { alert(i); } -]; -``` - -Этот массив возвращается из функции. -
      4. -
      5. Вызов `army[5]()` -- это получение элемента массива (им будет функция), и тут же -- её запуск.
      6. -
      - -# Почему ошибка - -Вначале разберемся, почему все стрелки выводят одно и то же значение. - -В функциях-стрелках `shooter` отсутствует переменная `i`. Когда такая функция вызывается, то `i` она берет из внешнего `LexicalEnvironment`. - -Чему же будет равно это значение `i`? - -К моменту вызова `army[0]()`, функция `makeArmy` уже закончила работу. Цикл завершился, последнее значение было `i=10`. - -В результате все функции `shooter` получают из внешнего лексического кружения это, одно и то же, последнее, значение `i=10`. - -Попробуйте исправить проблему самостоятельно. - -# Исправление (3 варианта) - -Есть несколько способов исправить ситуацию. - -
        -
      1. **Первый способ исправить код - это привязать значение непосредственно к функции-стрелку:** - -```js -//+ run -function makeArmy() { - - var shooters = []; - - for (var i = 0; i < 10; i++) { - -*!* - var shooter = function me() { - alert( me.i ); - }; - shooter.i = i; -*/!* - - shooters.push(shooter); - } - - return shooters; -} - -var army = makeArmy(); - -army[0](); // 0 -army[1](); // 1 -``` - -В этом случае каждая функция хранит в себе свой собственный номер. - -Кстати, обратите внимание на использование Named Function Expression, вот в этом участке: - -```js -... -var shooter = function me() { - alert( me.i ); -}; -... -``` - -Если убрать имя `me` и оставить обращение через `shooter`, то работать не будет: - -```js -for (var i = 0; i < 10; i++) { - var shooter = function() { -*!* - alert( shooter.i ); // вывести свой номер (не работает!) - // потому что откуда функция возьмёт переменную shooter? - // ..правильно, из внешнего объекта, а там она одна на всех -*/!* - }; - shooter.i = i; - shooters.push(shooter); -} -``` - -Вызов `alert(shooter.i)` при вызове будет искать переменную `shooter`, а эта переменная меняет значение по ходу цикла, и к моменту вызову она равна последней функции, созданной в цикле. - -Если использовать Named Function Expression, то имя жёстко привязывается к конкретной функции, и поэтому в коде выше `me.i` возвращает правильный `i`. - -
      2. -
      3. **Другое, более продвинутое решение -- использовать дополнительную функцию для того, чтобы "поймать" текущее значение `i`**: - -```js -//+ run -function makeArmy() { - - var shooters = []; - - for (var i = 0; i < 10; i++) { - -*!* - var shooter = (function(x) { - - return function() { - alert( x ); - }; - - })(i); -*/!* - - shooters.push(shooter); - } - - return shooters; -} - -var army = makeArmy(); - -army[0](); // 0 -army[1](); // 1 -``` - -Посмотрим выделенный фрагмент более внимательно, чтобы понять, что происходит: - -```js -var shooter = (function(x) { - return function() { - alert( x ); - }; -})(i); -``` - -Функция `shooter` создана как результат вызова промежуточного функционального выражения `function(x)`, которое объявляется -- и тут же выполняется, получая `x = i`. - -Так как `function(x)` тут же завершается, то значение `x` больше не меняется. Оно и будет использовано в возвращаемой функции-стрелке. - -Для красоты можно изменить название переменной `x` на `i`, суть происходящего при этом не изменится: - -```js -var shooter = (function(i) { - return function() { - alert( i ); - }; -})(i); -``` - -**Кстати, обратите внимание -- скобки вокруг `function(i)` не нужны**, можно и так: - -```js -var shooter = function(i) { // *!*без скобок вокруг function(i)*/!* - return function() { - alert( i ); - }; -}(i); -``` - -Скобки добавлены в код для лучшей читаемости, чтобы человек, который просматривает его, не подумал, что `var shooter = function`, а понял что это вызов "на месте", и присваивается его результат. -
      4. -
      5. **Еще один забавный способ - обернуть весь цикл во временную функцию**: - -```js -//+ run -function makeArmy() { - - var shooters = []; - -*!* - for (var i = 0; i < 10; i++)(function(i) { - - var shooter = function() { - alert( i ); - }; - - shooters.push(shooter); - - })(i); -*/!* - - return shooters; -} - -var army = makeArmy(); - -army[0](); // 0 -army[1](); // 1 -``` - -Вызов `(function(i) { ... })` обернут в скобки, чтобы интерпретатор понял, что это `Function Expression`. - -Плюс этого способа - в большей читаемости. Фактически, мы не меняем создание `shooter`, а просто обертываем итерацию в функцию. -
      6. -
      \ No newline at end of file diff --git a/1-js/5-functions-closures/4-closures-usage/6-make-army/task.md b/1-js/5-functions-closures/4-closures-usage/6-make-army/task.md deleted file mode 100644 index 15e0d2ae..00000000 --- a/1-js/5-functions-closures/4-closures-usage/6-make-army/task.md +++ /dev/null @@ -1,31 +0,0 @@ -# Армия функций - -[importance 5] - -Следующий код создает массив функций-стрелков `shooters`. По замыслу, каждый стрелок должен выводить свой номер: - -```js -//+ run -function makeArmy() { - - var shooters = []; - - for (var i = 0; i < 10; i++) { - var shooter = function() { // функция-стрелок - alert( i ); // выводит свой номер - }; - shooters.push(shooter); - } - - return shooters; -} - -var army = makeArmy(); - -army[0](); // стрелок выводит 10, а должен 0 -army[5](); // стрелок выводит 10... -// .. все стрелки выводят 10 вместо 0,1,2...9 -``` - -Почему все стрелки́ выводят одно и то же? Поправьте код, чтобы стрелки работали как задумано. Предложите несколько вариантов исправления. - diff --git a/1-js/5-functions-closures/4-closures-usage/article.md b/1-js/5-functions-closures/4-closures-usage/article.md deleted file mode 100644 index 0fa5da2c..00000000 --- a/1-js/5-functions-closures/4-closures-usage/article.md +++ /dev/null @@ -1,125 +0,0 @@ -# Локальные переменные для объекта - -Замыкания можно использовать сотнями способов. Иногда люди сами не замечают, что использовали замыкания -- настолько это просто и естественно. - -В этой главе мы рассмотрим дополнительные примеры использования замыканий и задачи на эту тему. - -[cut] - -## Счётчик-объект - -Ранее мы сделали счётчик. - -Напомню, как он выглядел: - -```js -//+ run -function makeCounter() { - var currentCount = 1; - - return function() { - return currentCount++; - }; -} - -var counter = makeCounter(); - -// каждый вызов возвращает результат, увеличивая счётчик -alert( counter() ); // 1 -alert( counter() ); // 2 -alert( counter() ); // 3 -``` - -Счётчик получился вполне рабочий, но вот только возможностей ему не хватает. Хорошо бы, чтобы можно было сбрасывать значение счётчика или начинать отсчёт с другого значения вместо `1` или... Да много чего можно захотеть от простого счётчика и, тем более, в более сложных проектах. - -**Чтобы добавить счётчику возможностей -- перейдём с функции на полноценный объект:** - -```js -//+ run -function makeCounter() { - var currentCount = 1; - - return { // возвратим объект вместо функции - getNext: function() { - return currentCount++; - }, - - set: function(value) { - currentCount = value; - }, - - reset: function() { - currentCount = 1; - } - }; -} - -var counter = makeCounter(); - -alert( counter.getNext() ); // 1 -alert( counter.getNext() ); // 2 - -counter.set(5); -alert( counter.getNext() ); // 5 -``` - -Теперь функция `makeCounter` возвращает не одну функцию, а объект с несколькими методами: - -
        -
      • `getNext()` -- получить следующее значение, то, что раньше делал вызов `counter()`.
      • -
      • `set(value)` -- поставить значение.
      • -
      • `reset()` -- обнулить счётчик.
      • -
      - -Все они получают ссылку `[[Scope]]` на текущий (внешний) объект переменных. Поэтому вызов любого из этих методов будет получать или модифицировать одно и то же внешнее значение `currentCount`. - -## Объект счётчика + функция - -Изначально, счётчик делался функцией во многом ради красивого вызова: `counter()`, который увеличивал значение и возвращал результат. - -К сожалению, при переходе на объект короткий вызов пропал, вместо него теперь `counter.getNext()`. Но он ведь был таким простым и удобным... - -Поэтому давайте вернём его! - -```js -//+ run -function makeCounter() { - var currentCount = 1; - -*!* - // возвращаемся к функции - function counter() { - return currentCount++; - } -*/!* - - // ...и добавляем ей методы! - counter.set = function(value) { - currentCount = value; - }; - - counter.reset = function() { - currentCount = 0; - }; - - return counter; -} - -var counter = makeCounter(); - -*!* -alert( counter() ); // 1 -alert( counter() ); // 2 - -counter.set(5); -alert( counter() ); // 5 -*/!* -``` - -Красиво, не правда ли? Получился полноценный объект, который можно вдобавок ещё и вызывать. - -Этот трюк часто используется при разработке JavaScript-библиотек. Например, популярная библиотека [jQuery](http://jquery.com) предоставляет глобальную переменную с именем [jQuery](http://api.jquery.com/jQuery/) (доступна также под коротким именем `$`), которая с одной стороны является функцией и может вызываться как `jQuery(...)`, а с другой -- у неё есть различные методы, например `jQuery.type(123)` возвращает тип аргумента. - -Далее вы найдёте различные задачи на понимание замыканий. Рекомендуется их сделать самостоятельно. - - diff --git a/1-js/5-functions-closures/5-closures-module/article.md b/1-js/5-functions-closures/5-closures-module/article.md deleted file mode 100644 index 384a86fb..00000000 --- a/1-js/5-functions-closures/5-closures-module/article.md +++ /dev/null @@ -1,322 +0,0 @@ -# Модули через замыкания - -Приём программирования "модуль" имеет громадное количество вариаций. Он немного похож на счётчик, который мы рассматривали ранее, использует аналогичный приём, но на уровне выше. - -Его цель -- скрыть внутренние детали реализации скрипта. В том числе: временные переменные, константы, вспомогательные мини-функции и т.п. - -## Зачем нужен модуль? - -Допустим, мы хотим разработать скрипт, который делает что-то полезное на странице. - -Умея работать со страницей, мы могли бы сделать много чего, но так как пока этого не было (скоро научимся), то пусть скрипт просто выводит сообщение: - -Файл `hello.js` - -```js -//+ run -// глобальная переменная нашего скрипта -var message = "Привет"; - -// функция для вывода этой переменной -function showMessage() { - alert( message ); -} - -// выводим сообщение -showMessage(); -``` - -У этого скрипта есть свои внутренние переменные и функции. - -В данном случае это `message` и `showMessage`. - -Предположим, что мы хотели бы распространять этот скрипт в виде библиотеки. Каждый, кто хочет, чтобы посетителям выдавалось "Привет" -- может просто подключить этот скрипт. Достаточно скачать и подключить, например, как внешний файл `hello.js` -- и готово. - -**Если подключить подобный скрипт к странице "как есть", то возможен конфликт с переменными, которые она использует.** - -То есть, при подключении к такой странице он её "сломает": - -```html - - - - - -``` - -[edit src="hello-conflict"/] - -Автор страницы ожидает, что библиотека `"hello.js"` просто отработает, без побочных эффектов. А она вместе с этим переопределила `message` в `"Привет"`. - -Если же убрать скрипт `hello.js`, то страница будет выводить правильное сообщение. - -Зная внутреннее устройство `hello.js` нам, конечно, понятно, что проблема возникла потому, что переменная `message` из скрипта `hello.js` перезаписала объявленную на странице. - -## Приём проектирования "Модуль" - -Чтобы проблемы не было, всего-то нужно, чтобы у скрипта была *своя собственная область видимости*, чтобы его переменные не попали на страницу. - -Для этого мы завернём всё его содержимое в функцию, которую тут же запустим. - -Файл `hello.js`, оформленный как модуль: - -```js -//+ run -(function() { - - // глобальная переменная нашего скрипта - var message = "Привет"; - - // функция для вывода этой переменной - function showMessage() { - alert( message ); - } - - // выводим сообщение - showMessage(); - -})(); -``` - -[edit src="hello-module"/] - -Этот скрипт при подключении к той же странице будет работать корректно. - -Будет выводиться "Привет", а затем "Пожалуйста, нажмите на кнопку". - - -### Зачем скобки вокруг функции? - -В примере выше объявление модуля выглядит так: - -```js -//+ run -(function() { - - alert( "объявляем локальные переменные, функции, работаем" ); - // ... - -}()); -``` - -В начале и в конце стоят скобки, так как иначе была бы ошибка. - -Вот, для сравнения, неверный вариант: - -```js -//+ run -function() { - // будет ошибка -}(); -``` - -Ошибка при его запуске произойдет потому, что браузер, видя ключевое слово `function` в основном потоке кода, попытается прочитать `Function Declaration`, а здесь имени нет. - -Впрочем, даже если имя поставить, то работать тоже не будет: - -```js -//+ run -function work() { - // ... -}(); // syntax error -``` - -**Дело в том, что "на месте" разрешено вызывать *только* `Function Expression`.** - -Общее правило таково: - -
        -
      • Если браузер видит `function` в основном потоке кода -- он считает, что это `Function Declaration`.
      • -
      • Если же `function` идёт в составе более сложного выражения, то он считает, что это `Function Expression`.
      • -
      - -Для этого и нужны скобки -- показать, что у нас `Function Expression`, который по правилам JavaScript можно вызвать "на месте". - -Можно показать это другим способом, например поставив перед функцией оператор: - -```js -//+ run no-beautify -+function() { - alert('Вызов на месте'); -}(); - -!function() { - alert('Так тоже будет работать'); -}(); -``` - -## Экспорт значения - -Приём "модуль" используется почти во всех современных библиотеках. - -Ведь что такое библиотека? Это полезные функции, ради которых её подключают, плюс временные переменные и вспомогательные функции, которые библиотека использует внутри себя. - -Посмотрим, к примеру, на библиотеку [Lodash](http://lodash.com/), хотя могли бы и [jQuery](http://jquery.com/), там почти то же самое. - -Если её подключить, то появится специальная переменная `lodash` (короткое имя `_`), которую можно использовать как функцию, и кроме того в неё записаны различные полезных свойства, например: - -
        -
      • `_.defaults(src, dst1, dst2...)` -- копирует в объект `src` те свойства из объектов `dst1`, `dst2` и других, которых там нет.
      • -
      • `_.cloneDeep(obj)` -- делает глубокое копирование объекта `obj`, создавая полностью независимый клон.
      • -
      • `_.size(obj)` -- возвращает количество свойств в объекте, полиморфная функция: можно передать массив или даже 1 значение.
      • -
      - - -Есть и много других функций, подробнее описанных в [документации](https://lodash.com/docs). - -Пример использования: - -```html - -

      Подключим библиотеку

      - - -

      Функция _.defaults() добавляет отсутствующие свойства.

      - -``` - - -Здесь нам не важно, какие, нас интересует именно как описана эта библиотека, как в ней применяется приём "модуль". - -Вот примерная выдержка из исходного файла: - -```js -//+ run no-beautify -;(function() { - -*!* - // lodash - основная функция для библиотеки -*/!* - function lodash(value) { - // ... - } - -*!* - // вспомогательная переменная -*/!* - var version = '2.4.1'; - // ... другие вспомогательные переменные и функции - -*!* - // код функции size, пока что доступен только внутри -*/!* - function size(collection) { - return Object.keys(collection).length; - } - -*!* - // присвоим в lodash size и другие функции, которые нужно вынести из модуля -*/!* - lodash.size = size - // lodash.defaults = ... - // lodash.cloneDeep = ... - -*!* - // "экспортировать" lodash наружу из модуля -*/!* - window._ = lodash; // в оригинальном коде здесь сложнее, но смысл тот же - -}()); -``` - -Внутри внешней функции: -
        -
      1. Происходит что угодно, объявляются свои локальные переменные, функции.
      2. -
      3. В `window` выносится то, что нужно снаружи.
      4. -
      - -Технически, мы могли бы вынести в `window` не только `lodash`, но и вообще все объекты и функции. На практике, как раз наоборот, всё прячут внутри модуля, глобальную область во избежание конфликтов хранят максимально чистой. - -[smart header="Зачем точка с запятой в начале?"] -В начале кода выше находится точка с запятой `;` -- это не опечатка, а особая "защита от дураков". - -Если получится, что несколько JS-файлы объединены в один (и, скорее всего, сжаты минификатором, но это не важно), и программист забыл поставить точку с запятой, то будет ошибка. - -Например, первый файл `a.js`: -```js -var a = 5 -``` - -Второй файл `lib.js`: -```js -//+ no-beautify -(function() { - // без точки с запятой в начале -})() -``` - -После объединения в один файл: - -```js -//+ run no-beautify -*!* -var a = 5 -*/!* - -// библиотека -(function() { - // ... -})(); -``` - -При запуске будет ошибка, потому что интерпретатор перед скобкой сам не вставит точку с запятой. Он просто поймёт код как `var a = 5(function ...)`, то есть пытается вызвать число `5` как функцию. - -Таковы правила языка, и поэтому рекомендуется явно ставить точку с запятой. В данном случае автор lodash ставит `;` перед функцией, чтобы предупредить эту ошибку. -[/smart] - - -## Экспорт через return - -Можно оформить модуль и чуть по-другому, например передать значение через `return`: - -```js -//+ no-beautify -var lodash = (function() { - - var version; - function assignDefaults() { ... } - - return { - defaults: function() { } - } - -})(); -``` - -Здесь, кстати, скобки вокруг внешней `function() { ... }` не обязательны, ведь функция и так объявлена внутри выражения присваивания, а значит -- является Function Expression. - -Тем не менее, лучше их ставить, для улучшения читаемости кода, чтобы было сразу видно, что это не простое присвоение функции. - -## Итого - -Модуль при помощи замыканий -- это оборачивание пакета функционала в единую внешнюю функцию, которая тут же выполняется. - -Все функции модуля будут иметь доступ к другим переменным и внутренним функциям этого же модуля через замыкание. - -Например, `defaults` из примера выше имеет доступ к `assignDefaults`. - -Но снаружи программист, использующий модуль, может обращаться напрямую только к тем, которые экспортированы. Благодаря этому будут скрыты внутренние аспекты реализации, которые нужны только разработчику модуля. - -Можно придумать и много других вариаций такого подхода. В конце концов, "модуль" -- это всего лишь функция-обёртка для скрытия переменных. - - diff --git a/1-js/5-functions-closures/5-closures-module/hello-conflict.view/hello.js b/1-js/5-functions-closures/5-closures-module/hello-conflict.view/hello.js deleted file mode 100755 index 56fa804b..00000000 --- a/1-js/5-functions-closures/5-closures-module/hello-conflict.view/hello.js +++ /dev/null @@ -1,10 +0,0 @@ -// глобальная переменная нашего скрипта -var message = "Привет"; - -// функция для вывода этой переменной -function showMessage() { - alert(message); -} - -// выводим сообщение -showMessage(); \ No newline at end of file diff --git a/1-js/5-functions-closures/5-closures-module/hello-conflict.view/index.html b/1-js/5-functions-closures/5-closures-module/hello-conflict.view/index.html deleted file mode 100755 index ab1ef1e2..00000000 --- a/1-js/5-functions-closures/5-closures-module/hello-conflict.view/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/5-functions-closures/5-closures-module/hello-module.view/hello.js b/1-js/5-functions-closures/5-closures-module/hello-module.view/hello.js deleted file mode 100755 index be40a230..00000000 --- a/1-js/5-functions-closures/5-closures-module/hello-module.view/hello.js +++ /dev/null @@ -1,14 +0,0 @@ -(function() { - - // глобальная переменная нашего скрипта - var message = "Привет"; - - // функция для вывода этой переменной - function showMessage() { - alert(message); - } - - // выводим сообщение - showMessage(); - -})(); \ No newline at end of file diff --git a/1-js/5-functions-closures/5-closures-module/hello-module.view/index.html b/1-js/5-functions-closures/5-closures-module/hello-module.view/index.html deleted file mode 100755 index ab1ef1e2..00000000 --- a/1-js/5-functions-closures/5-closures-module/hello-module.view/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/5-functions-closures/6-memory-management/article.md b/1-js/5-functions-closures/6-memory-management/article.md deleted file mode 100644 index 0a8c2b36..00000000 --- a/1-js/5-functions-closures/6-memory-management/article.md +++ /dev/null @@ -1,326 +0,0 @@ -# Управление памятью в JavaScript - -Управление памятью в JavaScript обычно происходит незаметно. Мы создаём примитивы, объекты, функции... Всё это занимает память. - -Что происходит с объектом, когда он становится "не нужен"? Возможно ли "переполнение" памяти? Для ответа на эти вопросы -- залезем "под капот" интерпретатора. - -[cut] -## Управление памятью в JavaScript - -Главной концепцией управления памятью в JavaScript является принцип *достижимости* (англ. reachability). - -
        -
      1. Определённое множество значений считается достижимым изначально, в частности: -
          -
        • Значения, ссылки на которые содержатся в стеке вызова, то есть -- все локальные переменные и параметры функций, которые в настоящий момент выполняются или находятся в ожидании окончания вложенного вызова.
        • -
        • Все глобальные переменные.
        • -
        - -Эти значения гарантированно хранятся в памяти. Мы будем называть их *корнями*. -
      2. -
      3. **Любое другое значение сохраняется в памяти лишь до тех пор, пока доступно из корня по ссылке или цепочке ссылок.**
      4. -
      - -Для очистки памяти от недостижимых значений в браузерах используется автоматический Сборщик мусора (англ. Garbage collection, GC), встроенный в интерпретатор, который наблюдает за объектами и время от времени удаляет недостижимые. - -Самая простая ситуация здесь с примитивами. При присвоении они копируются целиком, ссылок на них не создаётся, так что если в переменной была одна строка, а её заменили на другую, то предыдущую можно смело выбросить. - -Именно объекты требуют специального "сборщика мусора", который наблюдает за ссылками, так как на один объект может быть много ссылок из разных переменных и, при перезаписи одной из них, объект может быть всё ещё доступен из другой. - -Далее мы посмотрим ряд примеров, которые помогут в этом разобраться. - -### Достижимость и наличие ссылок - -Есть одно упрощение для работы с памятью: "значение остаётся в памяти, пока на него есть ссылка". - -Но такое упрощение будет верным лишь в одну сторону. - -
        -
      • **Верно -- в том плане, что если ссылок на значение нет, то память из-под него очищается.** - -Например, была создана ссылка в переменной, и эту переменную тут же перезаписали: - -```js -var user = { - name: "Вася" -}; -user = null; -``` - -Теперь объект `{ name: "Вася" }` более недоступен. Память будет освобождена. -
      • -
      • **Неверно -- в другую сторону: наличие ссылки не гарантирует, что значение останется в памяти.** - -Такая ситуация возникает с объектами, которые ссылаются друг на друга: - -```js -var vasya = {}; -var petya = {}; -vasya.friend = petya; -petya.friend = vasya; - -vasya = petya = null; -``` - -Несмотря на то, что на объекты `vasya`, `petya` ссылаются друг на друга через ссылку `friend`, то есть можно сказать, что на каждый из них есть ссылка, последняя строка делает эти объекты в совокупности недостижимыми. - -Поэтому они будут удалены из памяти. - -Здесь как раз и играет роль "достижимость" -- оба этих объекта становятся недостижимы из корней, в первую очередь, из глобальной области, стека. - -[Сборщик мусора](http://ru.wikipedia.org/wiki/%D0%A1%D0%B1%D0%BE%D1%80%D0%BA%D0%B0_%D0%BC%D1%83%D1%81%D0%BE%D1%80%D0%B0) отслеживает такие ситуации и очищает память. -
      • -
      - -## Алгоритм сборки мусора - -Сборщик мусора идёт от корня по ссылкам и запоминает все найденные объекты. По окончанию -- он смотрит, какие объекты в нём отсутствуют и удаляет их. - -Например, рассмотрим пример объекта "семья": - -```js -function marry(man, woman) { - woman.husband = man; - man.wife = woman; - - return { - father: man, - mother: woman - } -} - -var family = marry({ - name: "Василий" -}, { - name: "Мария" -}); -``` - -Функция `marry` принимает два объекта, даёт им ссылки друг на друга и возвращает третий, содержащий ссылки на оба. - -Получившийся объект `family` можно изобразить так: - - - -Здесь стрелочками показаны ссылки, а вот свойство `name` ссылкой не является, там хранится примитив, поэтому оно внутри самого объекта. - -Чтобы запустить сборщик мусора, удалим две ссылки: - -``` -delete family.father; -delete family.mother.husband; -``` - -Обратим внимание, удаление только одной из этих ссылок ни к чему бы не привело. Пока до объекта можно добраться из корня `window`, объект остаётся жив. - -А если две, то получается, что от бывшего `family.father` ссылки выходят, но в него -- ни одна не идёт: - - - -**Совершенно неважно, что из объекта выходят какие-то ссылки, они не влияют на достижимость этого объекта.** - -Бывший `family.father` стал недостижимым и будет удалён вместе со своми данными, которые также более недоступны из программы. - - - -А теперь -- рассмотрим более сложный случай. Что будет, если удалить главную ссылку `family`? - -Исходный объект -- тот же, что и в начале, а затем: - -```js -window.family = null; -``` - -Результат: - - - -Как видим, объекты в конструкции всё ещё связаны между собой. Однако, поиск от корня их не находит, они не достижимы, и значит сборщик мусора удалит их из памяти. - -[smart header="Оптимизации"] -Проблема описанного алгоритма -- в больших задержках. Если объектов много, то на поиск всех достижимых уйдёт довольно много времени. А ведь выполнение скрипта при этом должно быть остановлено, уже просканированные объекты не должны поменяться до окончания процесса. Получатся небольшие, но неприятные паузы-зависания в работе скрипта. - -Поэтому современные интерпретаторы применяют различные оптимизации. - -Самая частая -- это деление объектов на два вида "старые" и "новые". Для каждого типа выделяется своя область памяти. Каждый объект создаётся в "новой" области и, если прожил достаточно долго, мигрирует в старую. "Новая" область обычно небольшая. Она очищается часто. "Старая" -- редко. - -На практике получается эффективно, обычно большинство объектов создаются и умирают почти сразу, к примеру, служа локальными переменными функции: -```js -function showTime() { - alert( new Date() ); // этот объект будет создан и умрёт сразу -} -``` - -Если вы знаете низкоуровневые языки программирования, то более подробно об организации сборки мусора в V8 можно почитать, например, в статье [A tour of V8: Garbage Collection](http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection). - -[/smart] - -## Замыкания - -Объекты переменных, о которых шла речь ранее, в главе про замыкания, также подвержены сборке мусора. Они следуют тем же правилам, что и обычные объекты. - -Объект переменных внешней функции существует в памяти до тех пор, пока существует хоть одна внутренняя функция, ссылающаяся на него через свойство `[[Scope]]`. - -Например: - -
        -
      • Обычно объект переменных удаляется по завершении работы функции. Даже если в нём есть объявление внутренней функции: - -```js -function f() { - var value = 123; - - function g() {} // g видна только изнутри -} - -f(); -``` - -В коде выше `value` и `g` являются свойствами объекта переменных. Во время выполнения `f()` её объект переменных находится в текущем стеке выполнения, поэтому жив. По окончанию, он станет недостижимым и будет убран из памяти вместе с остальными локальными переменными. -
      • -
      • ...А вот в этом случае лексическое окружение, включая переменную `value`, будет сохранено: - -```js -function f() { - var value = 123; - - function g() {} - -*!* - return g; -*/!* -} - -var g = f(); // функция g будет жить и сохранит ссылку на объект переменных -``` - -В скрытом свойстве `g.[[Scope]]` находится ссылка на объект переменных, в котором была создана `g`. Поэтому этот объект переменных останется в памяти, а в нём -- и `value`. -
      • -
      • -Если `f()` будет вызываться много раз, а полученные функции будут сохраняться, например, складываться в массив, то будут сохраняться и объекты `LexicalEnvironment` с соответствующими значениями `value`: - -```js -function f() { - var value = Math.random(); - - return function() {}; -} - -// 3 функции, каждая ссылается на свой объект переменных, -// каждый со своим значением value -var arr = [f(), f(), f()]; -``` - -
      • -
      • Объект `LexicalEnvironment` живёт ровно до тех пор, пока на него существуют ссылки. В коде ниже после удаления ссылки на `g` умирает: - -```js -function f() { - var value = 123; - - function g() {} - - return g; -} - -var g = f(); // функция g жива -// а значит в памяти остается соответствующий объект переменных f() - -g = null; // ..а вот теперь память будет очищена -``` - -
      • -
      - -### Оптимизация в V8 и её последствия - -Современные JS-движки делают оптимизации замыканий по памяти. Они анализируют использование переменных и в случае, когда переменная из замыкания абсолютно точно не используется, удаляют её. - -В коде выше переменная `value` никак не используется. Поэтому она будет удалена из памяти. - -**Важный побочный эффект в V8 (Chrome, Opera) состоит в том, что удалённая переменная станет недоступна и при отладке!** - -Попробуйте запустить пример ниже с открытой консолью Chrome. Когда он остановится, в консоли наберите `alert(value)`. - -```js -//+ run -function f() { - var value = Math.random(); - - function g() { - debugger; // выполните в консоли alert( value ); Нет такой переменной! - } - - return g; -} - -var g = f(); -g(); -``` - -Как вы могли увидеть -- нет такой переменной! Недоступна она изнутри `g`. Интерпретатор решил, что она нам не понадобится и удалил. - -Это может привести к забавным казусам при отладке, вплоть до того что вместо этой переменной будет другая, внешняя: - -```js -//+ run -var value = "Сюрприз"; - -function f() { - var value = "самое близкое значение"; - - function g() { - debugger; // выполните в консоли alert( value ); Сюрприз! - } - - return g; -} - -var g = f(); -g(); -``` - -[warn header="Ещё увидимся"] -Об этой особенности важно знать. Если вы отлаживаете под Chrome/Opera, то наверняка рано или поздно с ней встретитесь! - -Это не глюк отладчика, а особенность работы V8, которая, возможно, будет когда-нибудь изменена. Вы всегда сможете проверить, не изменилось ли чего, запустив примеры на этой странице. -[/warn] - -## Влияние управления памятью на скорость - -На создание новых объектов и их удаление тратится время. Это важно иметь в виду в случае, когда важна производительность. - -В качестве примера рассмотрим рекурсию. При вложенных вызовах каждый раз создаётся новый объект с переменными и помещается в стек. Потом память из-под него нужно очистить. Поэтому рекурсивный код будет всегда медленнее использующего цикл, но насколько? - -Пример ниже тестирует сложение чисел до данного через рекурсию по сравнению с обычным циклом: - -```js -//+ run -function sumTo(n) { // обычный цикл 1+2+...+n - var result = 0; - for (var i = 1; i <= n; i++) { - result += i; - } - return result; -} - -function sumToRec(n) { // рекурсия sumToRec(n) = n+SumToRec(n-1) - return n == 1 ? 1 : n + sumToRec(n - 1); -} - -var timeLoop = performance.now(); -for (var i = 1; i < 1000; i++) sumTo(1000); // цикл -timeLoop = performance.now() - timeLoop; - -var timeRecursion = performance.now(); -for (var i = 1; i < 1000; i++) sumToRec(1000); // рекурсия -timeRecursion = performance.now() - timeRecursion; - -alert( "Разница в " + (timeRecursion / timeLoop) + " раз" ); -``` - -Различие в скорости на таком примере может составлять, в зависимости от интерпретатора, 2-10 раз. - -Вообще, этот пример -- не показателен. Ещё раз обращаю ваше внимание на то, что такие искусственные "микротесты" часто врут. Правильно их делать -- отдельная наука, которая выходит за рамки этой главы. Но и на практике ускорение в 2-10 раз оптимизацией по количеству объектов (и вообще, любых значений) -- отнюдь не миф, а вполне достижимо. - -В реальной жизни в большинстве ситуаций такая оптимизация несущественна, просто потому что "JavaScript и так достаточно быстр". Но она может быть эффективной для "узких мест" кода. diff --git a/1-js/5-functions-closures/6-memory-management/family-no-family.png b/1-js/5-functions-closures/6-memory-management/family-no-family.png deleted file mode 100644 index d2aeecb5..00000000 Binary files a/1-js/5-functions-closures/6-memory-management/family-no-family.png and /dev/null differ diff --git a/1-js/5-functions-closures/6-memory-management/family-no-family@2x.png b/1-js/5-functions-closures/6-memory-management/family-no-family@2x.png deleted file mode 100644 index 004d951d..00000000 Binary files a/1-js/5-functions-closures/6-memory-management/family-no-family@2x.png and /dev/null differ diff --git a/1-js/5-functions-closures/6-memory-management/family-no-father-2.png b/1-js/5-functions-closures/6-memory-management/family-no-father-2.png deleted file mode 100644 index 6abcd447..00000000 Binary files a/1-js/5-functions-closures/6-memory-management/family-no-father-2.png and /dev/null differ diff --git a/1-js/5-functions-closures/6-memory-management/family-no-father-2@2x.png b/1-js/5-functions-closures/6-memory-management/family-no-father-2@2x.png deleted file mode 100644 index de0506de..00000000 Binary files a/1-js/5-functions-closures/6-memory-management/family-no-father-2@2x.png and /dev/null differ diff --git a/1-js/5-functions-closures/6-memory-management/family-no-father.png b/1-js/5-functions-closures/6-memory-management/family-no-father.png deleted file mode 100644 index 59391ec6..00000000 Binary files a/1-js/5-functions-closures/6-memory-management/family-no-father.png and /dev/null differ diff --git a/1-js/5-functions-closures/6-memory-management/family-no-father@2x.png b/1-js/5-functions-closures/6-memory-management/family-no-father@2x.png deleted file mode 100644 index 2d10279e..00000000 Binary files a/1-js/5-functions-closures/6-memory-management/family-no-father@2x.png and /dev/null differ diff --git a/1-js/5-functions-closures/6-memory-management/family.png b/1-js/5-functions-closures/6-memory-management/family.png deleted file mode 100644 index eeb5c210..00000000 Binary files a/1-js/5-functions-closures/6-memory-management/family.png and /dev/null differ diff --git a/1-js/5-functions-closures/6-memory-management/family@2x.png b/1-js/5-functions-closures/6-memory-management/family@2x.png deleted file mode 100644 index 97d73db4..00000000 Binary files a/1-js/5-functions-closures/6-memory-management/family@2x.png and /dev/null differ diff --git a/1-js/5-functions-closures/7-with/1-with-function/solution.md b/1-js/5-functions-closures/7-with/1-with-function/solution.md deleted file mode 100644 index d4a5284b..00000000 --- a/1-js/5-functions-closures/7-with/1-with-function/solution.md +++ /dev/null @@ -1,21 +0,0 @@ -Вторая (`2`), т.к. при обращении к любой переменной внутри `with` -- она ищется прежде всего в объекте. - -Соответственно, будет выведено `2`: - -```js -//+ run -function f() { - alert(1) -} - -var obj = { - f: function() { - alert(2) - } -}; - -with(obj) { - f(); -} -``` - diff --git a/1-js/5-functions-closures/7-with/1-with-function/task.md b/1-js/5-functions-closures/7-with/1-with-function/task.md deleted file mode 100644 index bdf540ec..00000000 --- a/1-js/5-functions-closures/7-with/1-with-function/task.md +++ /dev/null @@ -1,22 +0,0 @@ -# With + функция - -[importance 5] - -Какая из функций будет вызвана? - -```js -function f() { - alert(1) -} - -var obj = { - f: function() { - alert(2) - } -}; - -with(obj) { - f(); -} -``` - diff --git a/1-js/5-functions-closures/7-with/2-with-variables/solution.md b/1-js/5-functions-closures/7-with/2-with-variables/solution.md deleted file mode 100644 index 531c8a81..00000000 --- a/1-js/5-functions-closures/7-with/2-with-variables/solution.md +++ /dev/null @@ -1,22 +0,0 @@ -Выведет `3`. - -**Конструкция `with` не создаёт области видимости,** её создают только функции. Поэтому объявление `var b` внутри конструкции работает также, как если бы оно было вне её. - -Код в задаче эквивалентен такому: - -```js -//+ run -var a = 1; -*!* -var b; -*/!* - -var obj = { - b: 2 -} - -with(obj) { - alert( a + b ); -} -``` - diff --git a/1-js/5-functions-closures/7-with/2-with-variables/task.md b/1-js/5-functions-closures/7-with/2-with-variables/task.md deleted file mode 100644 index 13705a1d..00000000 --- a/1-js/5-functions-closures/7-with/2-with-variables/task.md +++ /dev/null @@ -1,19 +0,0 @@ -# With + переменные - -[importance 5] - -Что выведет этот код? - -```js -var a = 1; - -var obj = { - b: 2 -}; - -with(obj) { - var b; - alert( a + b ); -} -``` - diff --git a/1-js/5-functions-closures/7-with/article.md b/1-js/5-functions-closures/7-with/article.md deleted file mode 100644 index 9c6434ff..00000000 --- a/1-js/5-functions-closures/7-with/article.md +++ /dev/null @@ -1,190 +0,0 @@ -# Устаревшая конструкция "with" - -Конструкция `with` позволяет использовать в качестве области видимости для переменных произвольный объект. - -В современном JavaScript от этой конструкции отказались. С `use strict` она не работает, но её ещё можно найти в старом коде, так что стоит познакомиться с ней, чтобы если что -- понимать, о чём речь. - -[cut] -Синтаксис: - -```js -with(obj) { - ...код... -} -``` - -Любое обращение к переменной внутри `with` сначала ищет её среди свойств `obj`, а только потом -- вне `with`. - -## Пример - -В примере ниже переменная будет взята не из глобальной области, а из `obj`: - -```js -//+ run -var a = 5; - -var obj = { - a: 10 -}; - -*!* -with(obj) { - alert( a ); // 10, из obj - } -*/!* -``` - -Попробуем получить переменную, которой в `obj` нет: - -```js -//+ run -var b = 1; - -var obj = { - a: 10 -}; - -*!* -with(obj) { - alert( b ); // 1, из window - } -*/!* -``` - -Здесь интерпретатор сначала проверяет наличие `obj.b`, не находит и идет вне `with`. - -Особенно забавно выглядит применение вложенных `with`: - -```js -//+ run -var obj = { - weight: 10, - size: { - width: 5, - height: 7 - } -}; - -with(obj) { - with(size) { // size будет взят из obj -*!* - alert( width * height / weight ); // width,height из size, weight из obj -*/!* - } -} -``` - -Свойства из разных объектов используются как обычные переменные... Магия! Порядок поиска переменных в выделенном коде: `size => obj => window`. - - -## Изменения переменной - -При использовании `with`, как и во вложенных функциях -- переменная изменяется в той области, где была найдена. - -Например: - -```js -//+ run -var obj = { - a: 10 -} - -*!* -with(obj) { - a = 20; - } -*/!* -alert( obj.a ); // 20, переменная была изменена в объекте -``` - -## Почему отказались от with? - -Есть несколько причин. - -
        -
      1. В современном стандарте `JavaScript` отказались от `with`, потому что конструкция `with` подвержена ошибкам и непрозрачна. - -Проблемы возникают в том случае, когда в `with(obj)` присваивается переменная, которая по замыслу должна быть в свойствах `obj`, но ее там нет. - -Например: - -```js -//+ run -var obj = { - weight: 10 -}; - -with(obj) { - weight = 20; // (1) - size = 35; // (2) -} - -alert( obj.size ); -alert( window.size ); -``` - -В строке `(2)` присваивается свойство, отсутствующее в `obj`. В результате интерпретатор, не найдя его, создает новую глобальную переменную `window.size`. - -Такие ошибки редки, но очень сложны в отладке, особенно если `size` изменилась не в `window`, а где-нибудь во внешнем `LexicalEnvironment`. -
      2. -
      3. Еще одна причина -- алгоритмы сжатия JavaScript не любят `with`. Перед выкладкой на сервер JavaScript сжимают. Для этого есть много инструментов, например [Closure Compiler](http://code.google.com/intl/ru-RU/closure/compiler/) и [UglifyJS](https://github.com/mishoo/UglifyJS). Обычно они переименовывают локальные переменные в более короткие имена, но не свойства объектов. С конструкцией `with` до запуска кода непонятно -- откуда будет взята переменная. Поэтому выходит, что, на всякий случай (если это свойство), лучше её не переименовывать. Таким образом, качество сжатия кода страдает.
      4. -
      5. Ну и, наконец, производительность -- усложнение поиска переменной из-за `with` влечет дополнительные накладные расходы. - -Современные движки применяют много внутренних оптимизаций, ряд которых не могут быть применены к коду, в котором есть `with`. - -Вот, к примеру, запустите этот код в современном браузере. Производительность функции `fast` существенно отличается `slow` с пустым(!) `with`. И дело тут именно в `with`, т.к. наличие этой конструкции препятствует оптимизации. - -```js -//+ run -var i = 0; - -function fast() { - i++; -} - -function slow() { - with(i) {} - i++; -} - - -var time = performance.now(); -while (i < 1000000) fast(); -alert( "Без with: " + (performance.now() - time) ); - -var time = performance.now(); -i = 0; -while (i < 1000000) slow(); -alert( "С with: " + (performance.now() - time) ); -``` - -
      6. -
      - -### Замена with - -Вместо `with` рекомендуется использовать временную переменную, например: - -```js -/* вместо -with(elem.style) { - top = '10px'; - left = '20px'; -} -*/ - -var s = elem.style; - -s.top = '10px'; -s.left = '0'; -``` - -Это не так элегантно, но убирает лишний уровень вложенности и абсолютно точно понятно, что будет происходить и куда присвоятся свойства. - -## Итого - -
        -
      • Конструкция `with(obj) { ... }` использует `obj` как дополнительную область видимости. Все переменные, к которым идет обращение внутри блока, сначала ищутся в `obj`.
      • -
      • Конструкция `with` устарела и не рекомендуется по ряду причин. Избегайте её.
      • -
      - diff --git a/1-js/5-functions-closures/index.md b/1-js/5-functions-closures/index.md deleted file mode 100644 index faff5532..00000000 --- a/1-js/5-functions-closures/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Замыкания, область видимости - -Понимание "области видимости" и "замыканий" -- ключевое в изучении JavaScript, без них "каши не сваришь". - -В этом разделе мы более глубоко изучаем переменные и функции -- и замыкания в том числе. \ No newline at end of file diff --git a/1-js/6-objects-more/1-object-methods/1-call-array-this/solution.md b/1-js/6-objects-more/1-object-methods/1-call-array-this/solution.md deleted file mode 100644 index dad88eea..00000000 --- a/1-js/6-objects-more/1-object-methods/1-call-array-this/solution.md +++ /dev/null @@ -1,15 +0,0 @@ -Вызов `arr[2]()` -- это обращение к методу объекта `obj[method]()`, в роли `obj` выступает `arr`, а в роли метода: `2`. - -Поэтому, как это бывает при вызове функции как метода, функция `arr[2]` получит `this = arr` и выведет массив: - -```js -//+ run -var arr = ["a", "b"]; - -arr.push(function() { - alert( this ); -}) - -arr[2](); // "a","b",function -``` - diff --git a/1-js/6-objects-more/1-object-methods/1-call-array-this/task.md b/1-js/6-objects-more/1-object-methods/1-call-array-this/task.md deleted file mode 100644 index 6ab74bfc..00000000 --- a/1-js/6-objects-more/1-object-methods/1-call-array-this/task.md +++ /dev/null @@ -1,16 +0,0 @@ -# Вызов в контексте массива - -[importance 5] - -Каким будет результат? Почему? - -```js -var arr = ["a", "b"]; - -arr.push(function() { - alert( this ); -}) - -arr[2](); // ? -``` - diff --git a/1-js/6-objects-more/1-object-methods/2-check-syntax/solution.md b/1-js/6-objects-more/1-object-methods/2-check-syntax/solution.md deleted file mode 100644 index b030823d..00000000 --- a/1-js/6-objects-more/1-object-methods/2-check-syntax/solution.md +++ /dev/null @@ -1,27 +0,0 @@ -**Ошибка**! - -Попробуйте: - -```js -//+ run -var obj = { - go: function() { - alert(this) - } -} - -(obj.go)() // error! -``` - -Причем сообщение об ошибке в большинстве браузеров не даёт понять, что на самом деле не так. - -**Ошибка возникла из-за того, что после объявления `obj` пропущена точка с запятой.** - -JavaScript игнорирует перевод строки перед скобкой `(obj.go)()` и читает этот код как: - -```js -//+ no-beautify -var obj = { go:... }(obj.go)() -``` - -Интерпретатор попытается вычислить это выражение, которое обозначает вызов объекта `{ go: ... }` как функции с аргументом `(obj.go)`. При этом, естественно, возникнет ошибка. diff --git a/1-js/6-objects-more/1-object-methods/2-check-syntax/task.md b/1-js/6-objects-more/1-object-methods/2-check-syntax/task.md deleted file mode 100644 index 44a93141..00000000 --- a/1-js/6-objects-more/1-object-methods/2-check-syntax/task.md +++ /dev/null @@ -1,16 +0,0 @@ -# Проверка синтаксиса - -[importance 2] - -Каков будет результат этого кода? - -```js -//+ no-beautify -var obj = { - go: function() { alert(this) } -} - -(obj.go)() -``` - -P.S. Есть подвох :) \ No newline at end of file diff --git a/1-js/6-objects-more/1-object-methods/3-why-this/solution.md b/1-js/6-objects-more/1-object-methods/3-why-this/solution.md deleted file mode 100644 index 693d3944..00000000 --- a/1-js/6-objects-more/1-object-methods/3-why-this/solution.md +++ /dev/null @@ -1,32 +0,0 @@ -
        -
      1. Обычный вызов функции в контексте объекта.
      2. -
      3. То же самое, скобки ни на что не влияют.
      4. -
      5. Здесь не просто вызов `obj.method()`, а более сложный вызов вида `(выражение).method()`. Такой вызов работает, как если бы он был разбит на две строки: - -```js -//+ no-beautify -f = obj.go; // сначала вычислить выражение -f(); // потом вызвать то, что получилось -``` - -При этом `f()` выполняется как обычная функция, без передачи `this`. -
      6. -
      7. Здесь также слева от точки находится выражение, вызов аналогичен двум строкам.
      8. -
      - -В спецификации это объясняется при помощи специального внутреннего типа [Reference Type](http://es5.github.com/x8.html#x8.7). - -Если подробнее -- то `obj.go()` состоит из двух операций: -
        -
      1. Сначала получить свойство `obj.go`.
      2. -
      3. Потом вызвать его как функцию.
      4. -
      - -Но откуда на шаге 2 получить `this`? Как раз для этого операция получения свойства `obj.go` возвращает значение особого типа `Reference Type`, который в дополнение к свойству `go` содержит информацию об `obj`. Далее, на втором шаге, вызов его при помощи скобок `()` правильно устанавливает `this`. - -**Любые другие операции, кроме вызова, превращают `Reference Type` в обычный тип, в данном случае -- функцию `go` (так уж этот тип устроен).** - -Поэтому получается, что `(method = obj.go)` присваивает в переменную `method` функцию `go`, уже без всякой информации об объекте `obj`. - -Аналогичная ситуация и в случае `(4)`: оператор ИЛИ `||` делает из `Reference Type` обычную функцию. - diff --git a/1-js/6-objects-more/1-object-methods/3-why-this/task.md b/1-js/6-objects-more/1-object-methods/3-why-this/task.md deleted file mode 100644 index 486c3eb7..00000000 --- a/1-js/6-objects-more/1-object-methods/3-why-this/task.md +++ /dev/null @@ -1,26 +0,0 @@ -# Почему this присваивается именно так? - -[importance 3] - -Вызовы `(1)` и `(2)` в примере ниже работают не так, как `(3)` и `(4)`: - -```js -//+ run no-beautify -"use strict" - -var obj, f; - -obj = { - go: function() { alert(this); } -}; - -obj.go(); // (1) object - -(obj.go)(); // (2) object - -(method = obj.go)(); // (3) undefined - -(obj.go || obj.stop)(); // (4) undefined -``` - -В чём дело? Объясните логику работы `this`. diff --git a/1-js/6-objects-more/1-object-methods/4-object-property-this/solution.md b/1-js/6-objects-more/1-object-methods/4-object-property-this/solution.md deleted file mode 100644 index 82f562d9..00000000 --- a/1-js/6-objects-more/1-object-methods/4-object-property-this/solution.md +++ /dev/null @@ -1,22 +0,0 @@ -**Ответ: пустая строка.** - -```js -//+ run -var name = ""; - -var user = { - name: "Василий", - -*!* - export: this // (*) -*/!* -}; - -alert( user.export.name ); -``` - -Объявление объекта само по себе не влияет на `this`. Никаких функций, которые могли бы повлиять на контекст, здесь нет. - -Так как код находится вообще вне любых функций, то `this` в нём равен `window` (при `use strict` было бы `undefined`). - -Получается, что в строке `(*)` мы имеем `export: window`, так что далее `alert(user.export.name)` выводит свойство `window.name`, то есть глобальную переменную `name`, которая равна пустой строке. diff --git a/1-js/6-objects-more/1-object-methods/4-object-property-this/task.md b/1-js/6-objects-more/1-object-methods/4-object-property-this/task.md deleted file mode 100644 index 6b2ec0ac..00000000 --- a/1-js/6-objects-more/1-object-methods/4-object-property-this/task.md +++ /dev/null @@ -1,18 +0,0 @@ -# Значение this в объявлении объекта - -[importance 5] - -Что выведет `alert` в этом коде? Почему? - -```js -var name = ""; - -var user = { - name: "Василий", - - export: this -}; - -alert( user.export.name ); -``` - diff --git a/1-js/6-objects-more/1-object-methods/5-return-this/solution.md b/1-js/6-objects-more/1-object-methods/5-return-this/solution.md deleted file mode 100644 index b34977b4..00000000 --- a/1-js/6-objects-more/1-object-methods/5-return-this/solution.md +++ /dev/null @@ -1,5 +0,0 @@ -**Ответ: `Василий`.** - -Вызов `user.export()` использует `this`, который равен объекту до точки, то есть внутри `user.export()` строка `return this` возвращает объект `user`. - -В итоге выводится свойство `name` объекта `user`, равное `"Василий"`. diff --git a/1-js/6-objects-more/1-object-methods/5-return-this/task.md b/1-js/6-objects-more/1-object-methods/5-return-this/task.md deleted file mode 100644 index 03ebd8cb..00000000 --- a/1-js/6-objects-more/1-object-methods/5-return-this/task.md +++ /dev/null @@ -1,21 +0,0 @@ -# Возврат this - -[importance 5] - -Что выведет `alert` в этом коде? Почему? - -```js -var name = ""; - -var user = { - name: "Василий", - - export: function() { - return this; - } - -}; - -alert( user.export().name ); -``` - diff --git a/1-js/6-objects-more/1-object-methods/6-return-object-this/solution.md b/1-js/6-objects-more/1-object-methods/6-return-object-this/solution.md deleted file mode 100644 index 2df08422..00000000 --- a/1-js/6-objects-more/1-object-methods/6-return-object-this/solution.md +++ /dev/null @@ -1,26 +0,0 @@ -**Ответ: `Василий`.** - -Во время выполнения `user.export()` значение `this = user`. - -При создании объекта `{ value: this }`, в свойство `value` копируется ссылка на текущий контекст, то есть на `user`. - -Получается что `user.export().value == user`. - - -```js -//+ run -var name = ""; - -var user = { - name: "Василий", - - export: function() { - return { - value: this - }; - } - -}; - -alert( user.export().value == user ); // true -``` \ No newline at end of file diff --git a/1-js/6-objects-more/1-object-methods/6-return-object-this/task.md b/1-js/6-objects-more/1-object-methods/6-return-object-this/task.md deleted file mode 100644 index b78e6553..00000000 --- a/1-js/6-objects-more/1-object-methods/6-return-object-this/task.md +++ /dev/null @@ -1,23 +0,0 @@ -# Возврат объекта с this - -[importance 5] - -Что выведет `alert` в этом коде? Почему? - -```js -var name = ""; - -var user = { - name: "Василий", - - export: function() { - return { - value: this - }; - } - -}; - -alert( user.export().value.name ); -``` - diff --git a/1-js/6-objects-more/1-object-methods/7-calculator/_js.view/solution.js b/1-js/6-objects-more/1-object-methods/7-calculator/_js.view/solution.js deleted file mode 100644 index 5f276b9c..00000000 --- a/1-js/6-objects-more/1-object-methods/7-calculator/_js.view/solution.js +++ /dev/null @@ -1,14 +0,0 @@ -var calculator = { - sum: function() { - return this.a + this.b; - }, - - mul: function() { - return this.a * this.b; - }, - - read: function() { - this.a = +prompt('a?', 0); - this.b = +prompt('b?', 0); - } -} \ No newline at end of file diff --git a/1-js/6-objects-more/1-object-methods/7-calculator/_js.view/test.js b/1-js/6-objects-more/1-object-methods/7-calculator/_js.view/test.js deleted file mode 100644 index cd2c9b05..00000000 --- a/1-js/6-objects-more/1-object-methods/7-calculator/_js.view/test.js +++ /dev/null @@ -1,22 +0,0 @@ -sinon.stub(window, "prompt"); - -prompt.onCall(0).returns("2"); -prompt.onCall(1).returns("3"); - -describe("calculator", function() { - before(function() { - calculator.read(); - }); - - it("при вводе 2 и 3 сумма равна 5", function() { - assert.equal(calculator.sum(), 5); - }); - - it("при вводе 2 и 3 произведение равно 6", function() { - assert.equal(calculator.mul(), 6); - }); -}); - -after(function() { - prompt.restore(); -}); \ No newline at end of file diff --git a/1-js/6-objects-more/1-object-methods/7-calculator/solution.md b/1-js/6-objects-more/1-object-methods/7-calculator/solution.md deleted file mode 100644 index 1849cc94..00000000 --- a/1-js/6-objects-more/1-object-methods/7-calculator/solution.md +++ /dev/null @@ -1,24 +0,0 @@ - - -```js -//+ run demo -var calculator = { - sum: function() { - return this.a + this.b; - }, - - mul: function() { - return this.a * this.b; - }, - - read: function() { - this.a = +prompt('a?', 0); - this.b = +prompt('b?', 0); - } -} - -calculator.read(); -alert( calculator.sum() ); -alert( calculator.mul() ); -``` - diff --git a/1-js/6-objects-more/1-object-methods/7-calculator/task.md b/1-js/6-objects-more/1-object-methods/7-calculator/task.md deleted file mode 100644 index 5e5790fc..00000000 --- a/1-js/6-objects-more/1-object-methods/7-calculator/task.md +++ /dev/null @@ -1,22 +0,0 @@ -# Создайте калькулятор - -[importance 5] - -Создайте объект `calculator` с тремя методами: -
        -
      • `read()` запрашивает `prompt` два значения и сохраняет их как свойства объекта
      • -
      • `sum()` возвращает сумму этих двух значений
      • -
      • `mul()` возвращает произведение этих двух значений
      • -
      - -```js -var calculator = { - ...ваш код... -} - -calculator.read(); -alert( calculator.sum() ); -alert( calculator.mul() ); -``` - -[demo /] diff --git a/1-js/6-objects-more/1-object-methods/8-chain-calls/solution.md b/1-js/6-objects-more/1-object-methods/8-chain-calls/solution.md deleted file mode 100644 index 8d89b88e..00000000 --- a/1-js/6-objects-more/1-object-methods/8-chain-calls/solution.md +++ /dev/null @@ -1,23 +0,0 @@ -Решение состоит в том, чтобы каждый раз возвращать текущий объект. Это делается добавлением `return this` в конце каждого метода: - -```js -//+ run -var ladder = { - step: 0, - up: function() { - this.step++; - return this; - }, - down: function() { - this.step--; - return this; - }, - showStep: function() { - alert( this.step ); - return this; - } -} - -ladder.up().up().down().up().down().showStep(); // 1 -``` - diff --git a/1-js/6-objects-more/1-object-methods/8-chain-calls/task.md b/1-js/6-objects-more/1-object-methods/8-chain-calls/task.md deleted file mode 100644 index 73dc4e8e..00000000 --- a/1-js/6-objects-more/1-object-methods/8-chain-calls/task.md +++ /dev/null @@ -1,40 +0,0 @@ -# Цепочка вызовов - -[importance 2] - -Есть объект "лестница" ladder: - -```js -var ladder = { - step: 0, - up: function() { // вверх по лестнице - this.step++; - }, - down: function() { // вниз по лестнице - this.step--; - }, - showStep: function() { // вывести текущую ступеньку - alert( this.step ); - } -}; -``` - -Сейчас, если нужно последовательно вызвать несколько методов объекта, это можно сделать так: - -```js -ladder.up(); -ladder.up(); -ladder.down(); -ladder.showStep(); // 1 -``` - -Модифицируйте код методов объекта, чтобы вызовы можно было делать цепочкой, вот так: - -```js -ladder.up().up().down().up().down().showStep(); // 1 -``` - -Как видно, такая запись содержит "меньше букв" и может быть более наглядной. - -Такой подход называется "чейнинг" (chaining) и используется, например, во фреймворке jQuery. - diff --git a/1-js/6-objects-more/1-object-methods/article.md b/1-js/6-objects-more/1-object-methods/article.md deleted file mode 100644 index 4a70f595..00000000 --- a/1-js/6-objects-more/1-object-methods/article.md +++ /dev/null @@ -1,244 +0,0 @@ -# Методы объектов, this - -До этого мы говорили об объекте лишь как о хранилище значений. Теперь пойдём дальше и поговорим об объектах как о сущностях со своими функциями ("методами"). -[cut] - -## Методы у объектов - -При объявлении объекта можно указать свойство-функцию, например: - -```js -//+ run -var user = { - name: 'Василий', - -*!* - // метод -*/!* - sayHi: function() { - alert( 'Привет!' ); - } - -}; - -*!* -// Вызов -user.sayHi(); -*/!* -``` - -Свойства-функции называют "методами" объектов. Их можно добавлять и удалять в любой момент, в том числе и явным присваиванием: - -```js -//+ run -var user = { - name: 'Василий' -}; - -*!* -user.sayHi = function() { // присвоили метод после создания объекта - alert('Привет!'); -}; -*/!* - -// Вызов метода: -*!*user.sayHi();*/!* -``` - -## Доступ к объекту через this - -Для полноценной работы метод должен иметь доступ к данным объекта. В частности, вызов `user.sayHi()` может захотеть вывести имя пользователя. - -**Для доступа к текущему объекту из метода используется ключевое слово `this`**. - -Значением `this` является объект перед "точкой", в контексте которого вызван метод, например: - -```js -//+ run -var user = { - name: 'Василий', - - sayHi: function() { - alert( *!*this.name*/!* ); - } -}; - -user.sayHi(); // sayHi в контексте user -``` - -Здесь при выполнении функции `user.sayHi()` в `this` будет храниться ссылка на текущий объект `user`. - -Вместо `this` внутри `sayHi` можно было бы обратиться к объекту, используя переменную `user`: - -```js -... - sayHi: function() { - alert( *!*user.name*/!* ); - } -... -``` - -...Однако, такое решение нестабильно. Если мы решим скопировать объект в другую переменную, например `admin = user`, а в переменную `user` записать что-то другое -- обращение будет совсем не по адресу: - -```js -//+ run -var user = { - name: 'Василий', - - sayHi: function() { - alert( *!*user.name*/!* ); // приведёт к ошибке - } -}; - -var admin = user; -user = null; - -admin.sayHi(); // упс! внутри sayHi обращение по старому имени, ошибка! -``` - -Использование `this` гарантирует, что функция работает именно с тем объектом, в контексте которого вызвана. - -Через `this` метод может не только обратиться к любому свойству объекта, но и передать куда-то ссылку на сам объект целиком: - -```js -//+ run no-beautify -var user = { - name: 'Василий', - -*!* - sayHi: function() { - showName(this); // передать текущий объект в showName - } -*/!* -}; - -function showName(namedObj) { - alert( namedObj.name ); -} - -user.sayHi(); // Василий -``` - -## Подробнее про this - -Любая функция может иметь в себе `this`. Совершенно неважно, объявлена ли она в объекте или отдельно от него. - -Значение `this` называется *контекстом вызова* и будет определено в момент вызова функции. - -Например, такая функция, объявленная без объекта, вполне допустима: - -```js -function sayHi() { - alert( *!*this.firstName*/!* ); -} -``` - -Эта функция ещё не знает, каким будет `this`. Это выяснится при выполнении программы. - -**Если одну и ту же функцию запускать в контексте разных объектов, она будет получать разный `this`:** - -```js -//+ run no-beautify -var user = { firstName: "Вася" }; -var admin = { firstName: "Админ" }; - -function func() { - alert( this.firstName ); -} - -user.f = func; -admin.g = func; - -*!* -// this равен объекту перед точкой: -user.f(); // Вася -admin.g(); // Админ -admin['g'](); // Админ (не важно, доступ к объекту через точку или квадратные скобки) -*/!* -``` - -Итак, значение `this` не зависит от того, как функция была создана, оно определяется исключительно в момент вызова. - -## Значение this при вызове без контекста - -Если функция использует `this` -- это подразумевает работу с объектом. Но и прямой вызов `func()` технически возможен. - -Как правило, такая ситуация возникает при ошибке в разработке. - -При этом `this` получает значение `window`, глобального объекта: - -```js -//+ run -function func() { - alert( this ); // выведет [object Window] или [object global] -} - -func(); -``` - -Таково поведение в старом стандарте. - -А в режиме `use strict` вместо глобального объекта `this` будет `undefined`: - -```js -//+ run -function func() { - "use strict"; - alert( this ); // выведет undefined (кроме IE9-) -} - -func(); -``` - -Обычно если в функции используется `this`, то она, всё же, служит для вызова в контексте объекта, так что такая ситуация -- скорее исключение. - -## Ссылочный тип - -Контекст `this` никак не привязан к функции, даже если она создана в объявлении объекта. Чтобы `this` передался, нужно вызвать функцию именно через точку (или квадратные скобки). - -Любой более хитрый вызов приведёт к потере контекста, например: - -```js -//+ run no-beautify -var user = { - name: "Вася", - hi: function() { alert(this.name); }, - bye: function() { alert("Пока"); } -}; - -user.hi(); // Вася (простой вызов работает) - -*!* -// а теперь вызовем user.hi или user.bye в зависимости от имени -(user.name == "Вася" ? user.hi : user.bye)(); // undefined -*/!* -``` - -В последней строке примера метод получен в результате выполнения тернарного оператора и тут же вызван. Но `this` при этом теряется. - -Если хочется понять, почему, то причина кроется в деталях работы вызова `obj.method()`. - -Он ведь, на самом деле, состоит из двух независимых операций: точка `.` -- получение свойства и скобки `()` -- его вызов (предполагается, что это функция). - -Функция, как мы говорили раньше, сама по себе не запоминает контекст. Чтобы "донести его" до скобок, JavaScript применяет "финт ушами" -- точка возвращает не функцию, а значение специального "ссылочного" типа [Reference Type](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-reference-specification-type). - -Этот тип представляет собой связку "base-name-strict", где: -
        -
      • *base* -- как раз объект,
      • -
      • *name* -- имя свойства,
      • -
      • *strict* -- вспомогательный флаг для передачи `use strict`.
      • -
      - -То есть, ссылочный тип (Reference Type) -- это своеобразное "три-в-одном". Он существует исключительно для целей спецификации, мы его не видим, поскольку любой оператор тут же от него избавляется: - -
        -
      • Скобки `()` получают из `base` значение свойства `name` и вызывают в контексте base.
      • -
      • Другие операторы получают из `base` значение свойства `name` и используют, а остальные компоненты игнорируют.
      • -
      - -Поэтому любая операция над результатом операции получения свойства, кроме вызова, приводит к потере контекста. - -Аналогично работает и получение свойства через квадратные скобки `obj[method]`. - - - diff --git a/1-js/6-objects-more/2-object-conversion/1-array-equals-string/solution.md b/1-js/6-objects-more/2-object-conversion/1-array-equals-string/solution.md deleted file mode 100644 index b7a70199..00000000 --- a/1-js/6-objects-more/2-object-conversion/1-array-equals-string/solution.md +++ /dev/null @@ -1,15 +0,0 @@ -Если с одной стороны -- объект, а с другой -- нет, то сначала приводится объект. - -В данном случае сравнение означает численное приведение. У массивов нет `valueOf`, поэтому вызывается `toString`, который возвращает список элементов через запятую. - -В данном случае, элемент только один - он и возвращается. Так что `['x']` становится `'x'`. Получилось `'x' == 'x'`, верно. - -P.S. -По той же причине верны равенства: - -```js -//+ run -alert( ['x', 'y'] == 'x,y' ); // true -alert( [] == '' ); // true -``` - diff --git a/1-js/6-objects-more/2-object-conversion/1-array-equals-string/task.md b/1-js/6-objects-more/2-object-conversion/1-array-equals-string/task.md deleted file mode 100644 index 190bbeb4..00000000 --- a/1-js/6-objects-more/2-object-conversion/1-array-equals-string/task.md +++ /dev/null @@ -1,11 +0,0 @@ -# ['x'] == 'x' - -[importance 5] - -Почему результат `true` ? - -```js -//+ run -alert( ['x'] == 'x' ); -``` - diff --git a/1-js/6-objects-more/2-object-conversion/2-tostring-valueof/solution.md b/1-js/6-objects-more/2-object-conversion/2-tostring-valueof/solution.md deleted file mode 100644 index 9b2108d0..00000000 --- a/1-js/6-objects-more/2-object-conversion/2-tostring-valueof/solution.md +++ /dev/null @@ -1,10 +0,0 @@ -# Первый alert(foo) - -Возвращает строковое представление объекта, используя `toString`, т.е. `"foo"`. - -# Второй alert(foo + 1) -Оператор `'+'` преобразует объект к примитиву, используя `valueOf`, так что результат: `3`. - -# Третий alert(foo + '3') - -То же самое, что и предыдущий случай, объект превращается в примитив `2`. Затем происходит сложение `2 + '3'`. Оператор `'+'` при сложении чего-либо со строкой приводит и второй операнд к строке, а затем применяет конкатенацию, так что результат -- строка `"23"`. \ No newline at end of file diff --git a/1-js/6-objects-more/2-object-conversion/2-tostring-valueof/task.md b/1-js/6-objects-more/2-object-conversion/2-tostring-valueof/task.md deleted file mode 100644 index 86bd17cb..00000000 --- a/1-js/6-objects-more/2-object-conversion/2-tostring-valueof/task.md +++ /dev/null @@ -1,24 +0,0 @@ -# Преобразование - -[importance 5] - -Объявлен объект с `toString` и `valueOf`. - -Какими будут результаты `alert`? - -```js -var foo = { - toString: function() { - return 'foo'; - }, - valueOf: function() { - return 2; - } -}; - -alert( foo ); -alert( foo + 1 ); -alert( foo + "3" ); -``` - -Подумайте, прежде чем ответить. \ No newline at end of file diff --git a/1-js/6-objects-more/2-object-conversion/3-compare-empty-arrays/solution.md b/1-js/6-objects-more/2-object-conversion/3-compare-empty-arrays/solution.md deleted file mode 100644 index ef69d31c..00000000 --- a/1-js/6-objects-more/2-object-conversion/3-compare-empty-arrays/solution.md +++ /dev/null @@ -1,34 +0,0 @@ -# Ответ по первому равенству - -Два объекта равны только тогда, когда это один и тот же объект. - -В первом равенстве создаются два массива, это разные объекты, так что они неравны. - -# Ответ по второму равенству - -
        -
      1. Первым делом, обе части сравнения вычисляются. Справа находится `![]`. Логическое НЕ `'!'` преобразует аргумент к логическому типу. Массив является объектом, так что это `true`. Значит, правая часть становится `![] = !true = false`. Так что получили: - -```js -alert( [] == false ); -``` - -
      2. -
      3. Проверка равенства между объектом и примитивом вызывает численное преобразование объекта. - -У массива нет `valueOf`, сработает `toString` и преобразует массив в список элементов, то есть - в пустую строку: - -```js -alert( '' == false ); -``` - -
      4. -
      5. Сравнение различных типов вызывает численное преобразование слева и справа: - -```js -alert( 0 == 0 ); -``` - -Теперь результат очевиден. -
      6. -
      \ No newline at end of file diff --git a/1-js/6-objects-more/2-object-conversion/3-compare-empty-arrays/task.md b/1-js/6-objects-more/2-object-conversion/3-compare-empty-arrays/task.md deleted file mode 100644 index 6945834b..00000000 --- a/1-js/6-objects-more/2-object-conversion/3-compare-empty-arrays/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Почему [] == [] неверно, а [ ] == ![ ] верно? - -[importance 5] - -Почему первое равенство -- неверно, а второе -- верно? - -```js -//+ run -alert( [] == [] ); // false -alert( [] == ![] ); // true -``` - -Какие преобразования происходят при вычислении? \ No newline at end of file diff --git a/1-js/6-objects-more/2-object-conversion/4-object-types-conversion-questions/solution.md b/1-js/6-objects-more/2-object-conversion/4-object-types-conversion-questions/solution.md deleted file mode 100644 index 3d7c6689..00000000 --- a/1-js/6-objects-more/2-object-conversion/4-object-types-conversion-questions/solution.md +++ /dev/null @@ -1,35 +0,0 @@ - - -```js -//+ no-beautify -new Date(0) - 0 = 0 // (1) -new Array(1)[0] + "" = "undefined" // (2) -({})[0]
 = undefined // (3) -[1] + 1 = "11" // (4) -[1,2] + [3,4] = "1,23,4" // (5) -[] + null + 1 = "null1" // (6) -[[0]][0][0] = 0 // (7) -({} + {}) = "[object Object][object Object]" // (8) -``` - -
        -
      1. `new Date(0)` -- дата, созданная по миллисекундам и соответствующая 0мс от 1 января 1970 года 00:00:00 UTC. Оператор минус `-` преобразует дату обратно в число миллисекунд, то есть в `0`.
      2. -
      3. `new Array(num)` при вызове с единственным аргументом-числом создаёт массив данной длины, без элементов. Поэтому его нулевой элемент равен `undefined`, при сложении со строкой получается строка `"undefined"`.
      4. -
      5. Фигурные скобки -- это создание пустого объекта, у него нет свойства `'0'`. Так что значением будет `undefined`. -Обратите внимание на внешние, круглые скобки. Если их убрать и запустить `{}[0]` в отладочной консоли браузера -- будет `0`, т.к. скобки `{}` будут восприняты как пустой блок кода, после которого идёт массив.
      6. -
      7. Массив преобразуется в строку `"1"`. Оператор `"+"` при сложении со строкой приводит второй аргумент к строке -- значит будет `"1" + "1" = "11"`.
      8. -
      9. Массивы приводятся к строке и складываются.
      10. -
      11. Массив преобразуется в пустую строку `"" + null + 1`, оператор `"+"` видит, что слева строка и преобразует `null` к строке, получается `"null" + 1`, и в итоге `"null1"`.
      12. -
      13. `[[0]]` -- это вложенный массив `[0]` внутри внешнего `[ ]`. Затем мы берём от него нулевой элемент, и потом еще раз. - -Если это непонятно, то посмотрите на такой пример: - -```js -//+ no-beautify -alert( [1,[0],2][1] ); -``` - -Квадратные скобки после массива/объекта обозначают не другой массив, а взятие элемента. -
      14. -
      15. Каждый объект преобразуется к примитиву. У встроенных объектов `Object` нет подходящего `valueOf`, поэтому используется `toString`, так что складываются в итоге строковые представления объектов.
      16. -
      diff --git a/1-js/6-objects-more/2-object-conversion/4-object-types-conversion-questions/task.md b/1-js/6-objects-more/2-object-conversion/4-object-types-conversion-questions/task.md deleted file mode 100644 index 85f767a5..00000000 --- a/1-js/6-objects-more/2-object-conversion/4-object-types-conversion-questions/task.md +++ /dev/null @@ -1,18 +0,0 @@ -# Вопросник по преобразованиям, для объектов - -[importance 5] - -Подумайте, какой результат будет у выражений ниже. Когда закончите -- сверьтесь с решением. - -```js -//+ no-beautify -new Date(0) - 0 -new Array(1)[0] + "" -({})[0]
 -[1] + 1 -[1,2] + [3,4] -[] + null + 1 -[[0]][0][0] -({} + {}) -``` - diff --git a/1-js/6-objects-more/2-object-conversion/5-sum-many-brackets/solution.md b/1-js/6-objects-more/2-object-conversion/5-sum-many-brackets/solution.md deleted file mode 100644 index 4b36c185..00000000 --- a/1-js/6-objects-more/2-object-conversion/5-sum-many-brackets/solution.md +++ /dev/null @@ -1,61 +0,0 @@ -# Подсказка - -Чтобы `sum(1)`, а также `sum(1)(2)` можно было вызвать новыми скобками -- результатом `sum` должна быть функция. - -Но эта функция также должна уметь превращаться в число. Для этого нужно дать ей соответствующий `valueOf`. А если мы хотим, чтобы и в строковом контексте она вела себя так же -- то `toString`. - -# Решение - -Функция, которая возвращается `sum`, должна накапливать значение при каждом вызове. - -Удобнее всего хранить его в замыкании, в переменной `currentSum`. Каждый вызов прибавляет к ней очередное значение: - -```js -//+ run -function sum(a) { - - var currentSum = a; - - function f(b) { - currentSum += b; - return f; - } - - f.toString = function() { - return currentSum; - }; - - return f; -} - -alert( sum(1)(2) ); // 3 -alert( sum(5)(-1)(2) ); // 6 -alert( sum(6)(-1)(-2)(-3) ); // 0 -alert( sum(0)(1)(2)(3)(4)(5) ); // 15 -``` - -При внимательном взгляде на решение легко заметить, что функция `sum` срабатывает только один раз. Она возвращает функцию `f`. - -Затем, при каждом запуске функция `f` добавляет параметр к сумме `currentSum`, хранящейся в замыкании, и возвращает сама себя. - -**В последней строчке `f` нет рекурсивного вызова.** - -Вот так была бы рекурсия: - -```js -function f(b) { - currentSum += b; - return f(); // <-- подвызов -} -``` - -А в нашем случае, мы просто возвращаем саму функцию, ничего не вызывая. - -```js -function f(b) { - currentSum += b; - return f; // <-- не вызывает сама себя, а возвращает ссылку на себя -} -``` - -Эта `f` используется при следующем вызове, опять возвратит себя, и так сколько нужно раз. Затем, при использовании в строчном или численном контексте -- сработает `toString`, который вернет текущую сумму `currentSum`. \ No newline at end of file diff --git a/1-js/6-objects-more/2-object-conversion/5-sum-many-brackets/task.md b/1-js/6-objects-more/2-object-conversion/5-sum-many-brackets/task.md deleted file mode 100644 index b220c704..00000000 --- a/1-js/6-objects-more/2-object-conversion/5-sum-many-brackets/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# Сумма произвольного количества скобок - -[importance 2] - -Напишите функцию `sum`, которая будет работать так: - -```js -sum(1)(2) == 3; // 1 + 2 -sum(1)(2)(3) == 6; // 1 + 2 + 3 -sum(5)(-1)(2) == 6 -sum(6)(-1)(-2)(-3) == 0 -sum(0)(1)(2)(3)(4)(5) == 15 -``` - -Количество скобок может быть любым. - -Пример такой функции для двух аргументов -- есть в решении задачи [](/task/closure-sum). \ No newline at end of file diff --git a/1-js/6-objects-more/2-object-conversion/article.md b/1-js/6-objects-more/2-object-conversion/article.md deleted file mode 100644 index e473266b..00000000 --- a/1-js/6-objects-more/2-object-conversion/article.md +++ /dev/null @@ -1,281 +0,0 @@ -# Преобразование объектов: toString и valueOf - -Ранее, в главе [](/types-conversion) мы рассматривали преобразование типов для примитивов. Теперь добавим в нашу картину мира объекты. - -Бывают операции, при которых объект должен быть преобразован в примитив. -[cut] -Например: - -
        -
      • Строковое преобразование -- если объект выводится через `alert(obj)`.
      • -
      • Численное преобразование -- при арифметических операциях, сравнении с примитивом.
      • -
      • Логическое преобразование -- при `if(obj)` и других логических операциях.
      • -
      - -Рассмотрим эти преобразования по очереди. - -## Логическое преобразование - -Проще всего -- с логическим преобразованием. - -**Любой объект в логическом контексте -- `true`, даже если это пустой массив `[]` или объект `{}`.** - -```js -//+ run -if ({} && []) { - alert( "Все объекты - true!" ); // alert сработает -} -``` - -## Строковое преобразование - -Строковое преобразование проще всего увидеть, если вывести объект при помощи `alert`: - -```js -//+ run -var user = { - firstName: 'Василий' -}; - -alert( user ); // [object Object] -``` - -Как видно, содержимое объекта не вывелось. Это потому, что стандартным строковым представлением пользовательского объекта является строка `"[object Object]"`. - -Такой вывод объекта не содержит интересной информации. Поэтому имеет смысл его поменять на что-то более полезное. - -**Если в объекте присутствует метод `toString`, который возвращает примитив, то он используется для преобразования.** - -```js -//+ run -var user = { - - firstName: 'Василий', - - *!*toString:*/!* function() { - return 'Пользователь ' + this.firstName; - } -}; - -alert( user ); // Пользователь Василий -``` - -[smart header="Результатом `toString` может быть любой примитив"] -Метод `toString` не обязан возвращать именно строку. - -Его результат может быть любого примитивного типа. Например, это может быть число, как в примере ниже: - -```js -//+ run -var obj = { - toString: function() { - return 123; - } -}; - -alert( obj ); // 123 -``` - -Поэтому мы и называем его здесь *"строковое преобразование"*, а не "преобразование к строке". -[/smart] - -Все объекты, включая встроенные, имеют свои реализации метода `toString`, например: - -```js -//+ run -alert( [1, 2] ); // toString для массивов выводит список элементов "1,2" -alert( new Date ); // toString для дат выводит дату в виде строки -alert( function() {} ); // toString для функции выводит её код -``` - -## Численное преобразование - -Для численного преобразования объекта используется метод `valueOf`, а если его нет -- то `toString`: - -```js -//+ run -var room = { - number: 777, - - valueOf: function() { return this.number; }, - toString: function() { return this.number; } -}; - -alert( +room ); // 777, *!*вызвался valueOf*/!* - -delete room.valueOf; // *!*valueOf удалён*/!* - -alert( +room ); // 777, *!*вызвался toString*/!* -``` - -Метод `valueOf` обязан возвращать примитивное значение, иначе его результат будет проигнорирован. При этом -- не обязательно числовое. - -[smart header="У большинства объектов нет `valueOf`"] -У большинства встроенных объектов такого `valueOf` нет, поэтому численное и строковое преобразования для них работают одинаково. - -Исключением является объект `Date`, который поддерживает оба типа преобразований: - -```js -//+ run -alert( new Date() ); // toString: Дата в виде читаемой строки -alert( +new Date() ); // valueOf: кол-во миллисекунд, прошедших с 01.01.1970 -``` - -[/smart] - -[smart header="Детали спецификации"] -Если посмотреть в стандарт, то в пункте [15.2.4.4](http://es5.github.com/x15.2.html#x15.2.4.4) говорится о том, что `valueOf` есть у любых объектов. Но он ничего не делает, просто возвращает сам объект (не-примитивное значение!), а потому игнорируется. -[/smart] - -## Две стадии преобразования - -Итак, объект преобразован в примитив при помощи `toString` или `valueOf`. - -Но на этом преобразования не обязательно заканчиваются. Вполне возможно, что в процессе вычислений этот примитив будет преобразован во что-то другое. - -Например, рассмотрим применение к объекту операции `==`: - -```js -//+ run -var obj = { - valueOf: function() { - return 1; - } -}; - -alert( obj == true ); // true -``` - -Объект `obj` был сначала преобразован в примитив, используя численное преобразование, получилось `1 == true`. - -Далее, так как значения всё ещё разных типов, применяются правила преобразования примитивов, результат: `true`. - -То же самое -- при сложении с объектом при помощи `+`: - -```js -//+ run -var obj = { - valueOf: function() { - return 1; - } -}; - -alert( obj + "test" ); // 1test -``` - -Или вот, для разности объектов: - -```js -//+ run -var a = { - valueOf: function() { - return "1"; - } -}; -var b = { - valueOf: function() { - return "2"; - } -}; - -alert( a + b ); // "12" -alert( a - b ); // "1" - "2" = -1 -``` - -[warn header="Исключение: `Date`"] -Объект `Date`, по историческим причинам, является исключением. - -Бинарный оператор плюс `+` обычно использует числовое преобразование и метод `valueOf`. Как мы уже знаем, если подходящего `valueOf` нет (а его нет у большинства объектов), то используется `toString`, так что в итоге преобразование происходит к строке. Но если есть `valueOf`, то используется `valueOf`. Выше в примере как раз `a + b` это демонстрируют. - -У объектов `Date` есть и `valueOf` -- возвращает количество миллисекунд, и `toString` -- возвращает строку с датой. - -...Но оператор `+` для `Date` использует именно `toString` (хотя должен бы `valueOf`). - -Это и есть исключение: - -```js -//+ run -// бинарный плюс для даты toString, для остальных объектов valueOf -alert( new Date + "" ); // "строка даты" -``` - -Других подобных исключений нет. -[/warn] - -[warn header="Как испугать Java-разработчика"] -В языке Java (это не JavaScript, другой язык, здесь приведён для примера) логические значения можно создавать, используя синтаксис `new Boolean(true/false)`, например `new Boolean(true)`. - -В JavaScript тоже есть подобная возможность, которая возвращает "объектную обёртку" для логического значения. - -Эта возможность давно существует лишь для совместимости, она и не используется на практике, поскольку приводит к странным результатам. Некоторые из них могут сильно удивить человека, не привыкшего к JavaScript, например: - -```js -//+ run -var value = new Boolean(false); -if (value) { - alert( true ); // сработает! -} -``` - -Почему запустился `alert`? Ведь в `if` находится `false`... Проверим: - -```js -//+ run -var value = new Boolean(false); - -*!* -alert( value ); // выводит false, все ок.. -*/!* - -if (value) { - alert( true ); // ..но тогда почему выполняется alert в if ?!? -} -``` - -Дело в том, что `new Boolean` -- это не примитивное значение, а объект. Поэтому в логическом контексте он преобразуется к `true`, в результате работает первый пример. - -А второй пример вызывает `alert`, который преобразует объект к строке, и он становится `"false"`. - -**В JavaScript вызовы `new Boolean/String/Number` не используются, а используются простые вызовы соответствующих функций, они преобразуют значение в примитив нужного типа, например `Boolean(val) === !!val`.** -[/warn] - -## Итого - -
        -
      • В логическом контексте объект -- всегда `true`.
      • -
      • При строковом преобразовании объекта используется его метод `toString`. Он должен возвращать примитивное значение, причём не обязательно именно строку. -
      • -
      • Для численного преобразования используется метод `valueOf`, который также может возвратить любое примитивное значение. У большинства объектов `valueOf` не работает (возвращает сам объект и потому игнорируется), при этом для численного преобразования используется `toString`.
      • -
      - -Полный алгоритм преобразований есть в спецификации EcmaScript, смотрите пункты [11.8.5](http://es5.github.com/x11.html#x11.8.5), [11.9.3](http://es5.github.com/x11.html#x11.9.3), а также [9.1](http://es5.github.com/x9.html#x9.1) и [9.3](http://es5.github.com/x9.html#x9.3). - - -Заметим, для полноты картины, что некоторые тесты знаний в интернет предлагают вопросы типа: -```js -//+ no-beautify -{}[0] // чему равно? -{} + {} // а так? -``` - -Если вы запустите эти выражения в консоли, то результат может показаться странным. Подвох здесь в том, что если фигурные скобки `{...}` идут не в выражении, а в основном потоке кода, то JavaScript считает, что это не объект, а "блок кода" (как `if`, `for`, но без оператора, просто группировка команд вместе, используется редко). - -Вот блок кода с командой: -```js -//+run -{ - alert("Блок") -} -``` - -А если команду изъять, то будет пустой блок `{}`, который ничего не делает. Два примера выше как раз содержат пустой блок в начале, который ничего не делает. Иначе говоря: -```js -//+ no-beautify -{}[0] // то же что и: [0] -{} + {} // то же что и: + {} -``` - -То есть, такие вопросы -- не на преобразование типов, а на понимание, что если `{ ... }` находится вне выражений, то это не объект, а блок. - - - diff --git a/1-js/6-objects-more/3-constructor-new/1-two-functions-one-object/solution.md b/1-js/6-objects-more/3-constructor-new/1-two-functions-one-object/solution.md deleted file mode 100644 index 5b27cec2..00000000 --- a/1-js/6-objects-more/3-constructor-new/1-two-functions-one-object/solution.md +++ /dev/null @@ -1,19 +0,0 @@ -Да, возможны. - -Они должны возвращать одинаковый объект. При этом если функция возвращает объект, то `this` не используется. - -Например, они могут вернуть один и тот же объект `obj`, определённый снаружи: - -```js -//+ run no-beautify -var obj = {}; - -function A() { return obj; } -function B() { return obj; } - -var a = new A; -var b = new B; - -alert( a == b ); // true -``` - diff --git a/1-js/6-objects-more/3-constructor-new/1-two-functions-one-object/task.md b/1-js/6-objects-more/3-constructor-new/1-two-functions-one-object/task.md deleted file mode 100644 index 72c4e8eb..00000000 --- a/1-js/6-objects-more/3-constructor-new/1-two-functions-one-object/task.md +++ /dev/null @@ -1,18 +0,0 @@ -# Две функции один объект - -[importance 2] - -Возможны ли такие функции `A` и `B` в примере ниже, что соответствующие объекты `a,b` равны (см. код ниже)? - -```js -//+ no-beautify -function A() { ... } -function B() { ... } - -var a = new A; -var b = new B; - -alert( a == b ); // true -``` - -Если да -- приведите пример кода с такими функциями. \ No newline at end of file diff --git a/1-js/6-objects-more/3-constructor-new/2-calculator-constructor/_js.view/test.js b/1-js/6-objects-more/3-constructor-new/2-calculator-constructor/_js.view/test.js deleted file mode 100644 index ef881c45..00000000 --- a/1-js/6-objects-more/3-constructor-new/2-calculator-constructor/_js.view/test.js +++ /dev/null @@ -1,25 +0,0 @@ -sinon.stub(window, "prompt") - -prompt.onCall(0).returns("2"); -prompt.onCall(1).returns("3"); - -describe("calculator", function() { - var calculator; - before(function() { - calculator = new Calculator(); - calculator.read(); - }); - - it("при вводе 2 и 3 сумма равна 5", function() { - assert.equal(calculator.sum(), 5); - }); - - it("при вводе 2 и 3 произведение равно 6", function() { - assert.equal(calculator.mul(), 6); - }); - -}); - -after(function() { - prompt.restore(); -}); \ No newline at end of file diff --git a/1-js/6-objects-more/3-constructor-new/2-calculator-constructor/solution.md b/1-js/6-objects-more/3-constructor-new/2-calculator-constructor/solution.md deleted file mode 100644 index a78010d9..00000000 --- a/1-js/6-objects-more/3-constructor-new/2-calculator-constructor/solution.md +++ /dev/null @@ -1,27 +0,0 @@ - - -```js -//+ run demo -function Calculator() { - - this.read = function() { - this.a = +prompt('a?', 0); - this.b = +prompt('b?', 0); - }; - - this.sum = function() { - return this.a + this.b; - }; - - this.mul = function() { - return this.a * this.b; - }; -} - -var calculator = new Calculator(); -calculator.read(); - -alert( "Сумма=" + calculator.sum() ); -alert( "Произведение=" + calculator.mul() ); -``` - diff --git a/1-js/6-objects-more/3-constructor-new/2-calculator-constructor/task.md b/1-js/6-objects-more/3-constructor-new/2-calculator-constructor/task.md deleted file mode 100644 index 31258cb7..00000000 --- a/1-js/6-objects-more/3-constructor-new/2-calculator-constructor/task.md +++ /dev/null @@ -1,22 +0,0 @@ -# Создать Calculator при помощи конструктора - -[importance 5] - -Напишите *функцию-конструктор* `Calculator`, которая создает объект с тремя методами: -
        -
      • Метод `read()` запрашивает два значения при помощи `prompt` и запоминает их в свойствах объекта.
      • -
      • Метод `sum()` возвращает сумму запомненных свойств.
      • -
      • Метод `mul()` возвращает произведение запомненных свойств.
      • -
      - -Пример использования: - -```js -var calculator = new Calculator(); -calculator.read(); - -alert( "Сумма=" + calculator.sum() ); -alert( "Произведение=" + calculator.mul() ); -``` - -[demo /] diff --git a/1-js/6-objects-more/3-constructor-new/3-accumulator/_js.view/solution.js b/1-js/6-objects-more/3-constructor-new/3-accumulator/_js.view/solution.js deleted file mode 100644 index bd744597..00000000 --- a/1-js/6-objects-more/3-constructor-new/3-accumulator/_js.view/solution.js +++ /dev/null @@ -1,8 +0,0 @@ -function Accumulator(startingValue) { - this.value = startingValue; - - this.read = function() { - this.value += +prompt('Сколько добавлять будем?', 0); - }; - -} \ No newline at end of file diff --git a/1-js/6-objects-more/3-constructor-new/3-accumulator/_js.view/test.js b/1-js/6-objects-more/3-constructor-new/3-accumulator/_js.view/test.js deleted file mode 100644 index aa651b11..00000000 --- a/1-js/6-objects-more/3-constructor-new/3-accumulator/_js.view/test.js +++ /dev/null @@ -1,37 +0,0 @@ -describe("Accumulator(1)", function() { - var accumulator; - before(function() { - accumulator = new Accumulator(1); - }); - - beforeEach(function() { - sinon.stub(window, "prompt") - }); - - afterEach(function() { - prompt.restore(); - }); - - it("начальное значение 1", function() { - assert.equal(accumulator.value, 1); - }); - - it("после ввода 0 значение 1", function() { - prompt.returns("0"); - accumulator.read(); - assert.equal(accumulator.value, 1); - }); - - it("после ввода 1 значение 2", function() { - prompt.returns("1"); - accumulator.read(); - assert.equal(accumulator.value, 2); - }); - - it("после ввода 2 значение 4", function() { - prompt.returns("2"); - accumulator.read(); - assert.equal(accumulator.value, 4); - }); - -}); \ No newline at end of file diff --git a/1-js/6-objects-more/3-constructor-new/3-accumulator/solution.md b/1-js/6-objects-more/3-constructor-new/3-accumulator/solution.md deleted file mode 100644 index f5b2ba43..00000000 --- a/1-js/6-objects-more/3-constructor-new/3-accumulator/solution.md +++ /dev/null @@ -1,19 +0,0 @@ - - -```js -//+ run demo -function Accumulator(startingValue) { - this.value = startingValue; - - this.read = function() { - this.value += +prompt('Сколько добавлять будем?', 0); - }; - -} - -var accumulator = new Accumulator(1); -accumulator.read(); -accumulator.read(); -alert( accumulator.value ); -``` - diff --git a/1-js/6-objects-more/3-constructor-new/3-accumulator/task.md b/1-js/6-objects-more/3-constructor-new/3-accumulator/task.md deleted file mode 100644 index 7852e561..00000000 --- a/1-js/6-objects-more/3-constructor-new/3-accumulator/task.md +++ /dev/null @@ -1,24 +0,0 @@ -# Создать Accumulator при помощи конструктора - -[importance 5] - -Напишите *функцию-конструктор* `Accumulator(startingValue)`. -Объекты, которые она создает, должны хранить текущую сумму и прибавлять к ней то, что вводит посетитель. - -Более формально, объект должен: -
        -
      • Хранить текущее значение в своём свойстве `value`. Начальное значение свойства `value` ставится конструктором равным `startingValue`.
      • -
      • Метод `read()` вызывает `prompt`, принимает число и прибавляет его к свойству `value`.
      • -
      -Таким образом, свойство `value` является текущей суммой всего, что ввел посетитель при вызовах метода `read()`, с учетом начального значения `startingValue`. - -Ниже вы можете посмотреть работу кода: - -```js -var accumulator = new Accumulator(1); // начальное значение 1 -accumulator.read(); // прибавит ввод prompt к текущему значению -accumulator.read(); // прибавит ввод prompt к текущему значению -alert( accumulator.value ); // выведет текущее значение -``` - -[demo /] diff --git a/1-js/6-objects-more/3-constructor-new/4-calculator-extendable/_js.view/solution.js b/1-js/6-objects-more/3-constructor-new/4-calculator-extendable/_js.view/solution.js deleted file mode 100644 index 4bf9f22b..00000000 --- a/1-js/6-objects-more/3-constructor-new/4-calculator-extendable/_js.view/solution.js +++ /dev/null @@ -1,29 +0,0 @@ -function Calculator() { - - var methods = { - "-": function(a, b) { - return a - b; - }, - "+": function(a, b) { - return a + b; - } - }; - - this.calculate = function(str) { - - var split = str.split(' '), - a = +split[0], - op = split[1], - b = +split[2] - - if (!methods[op] || isNaN(a) || isNaN(b)) { - return NaN; - } - - return methods[op](+a, +b); - } - - this.addMethod = function(name, func) { - methods[name] = func; - }; -} \ No newline at end of file diff --git a/1-js/6-objects-more/3-constructor-new/4-calculator-extendable/_js.view/test.js b/1-js/6-objects-more/3-constructor-new/4-calculator-extendable/_js.view/test.js deleted file mode 100644 index 2c6891a9..00000000 --- a/1-js/6-objects-more/3-constructor-new/4-calculator-extendable/_js.view/test.js +++ /dev/null @@ -1,26 +0,0 @@ -var calculator; -before(function() { - calculator = new Calculator; -}); - -it("calculate(12 + 34) = 46", function() { - assert.equal(calculator.calculate("12 + 34"), 46); -}); - -it("calculate(34 - 12) = 22", function() { - assert.equal(calculator.calculate("34 - 12"), 22); -}); - -it("добавили умножение: calculate(2 * 3) = 6", function() { - calculator.addMethod("*", function(a, b) { - return a * b; - }); - assert.equal(calculator.calculate("2 * 3"), 6); -}); - -it("добавили возведение в степень: calculate(2 ** 3) = 8", function() { - calculator.addMethod("**", function(a, b) { - return Math.pow(a, b); - }); - assert.equal(calculator.calculate("2 ** 3"), 8); -}); \ No newline at end of file diff --git a/1-js/6-objects-more/3-constructor-new/4-calculator-extendable/solution.md b/1-js/6-objects-more/3-constructor-new/4-calculator-extendable/solution.md deleted file mode 100644 index 3e5f26e5..00000000 --- a/1-js/6-objects-more/3-constructor-new/4-calculator-extendable/solution.md +++ /dev/null @@ -1,55 +0,0 @@ - - -```js -//+ run -function Calculator() { - - var methods = { - "-": function(a, b) { - return a - b; - }, - "+": function(a, b) { - return a + b; - } - }; - - this.calculate = function(str) { - - var split = str.split(' '), - a = +split[0], - op = split[1], - b = +split[2] - - if (!methods[op] || isNaN(a) || isNaN(b)) { - return NaN; - } - - return methods[op](+a, +b); - } - - this.addMethod = function(name, func) { - methods[name] = func; - }; -} - -var calc = new Calculator; - -calc.addMethod("*", function(a, b) { - return a * b; -}); -calc.addMethod("/", function(a, b) { - return a / b; -}); -calc.addMethod("**", function(a, b) { - return Math.pow(a, b); -}); - -var result = calc.calculate("2 ** 3"); -alert( result ); // 8 -``` - -
        -
      • Обратите внимание на хранение методов. Они просто добавляются к внутреннему объекту.
      • -
      • Все проверки и преобразование к числу производятся в методе `calculate`. В дальнейшем он может быть расширен для поддержки более сложных выражений.
      • -
      - diff --git a/1-js/6-objects-more/3-constructor-new/4-calculator-extendable/task.md b/1-js/6-objects-more/3-constructor-new/4-calculator-extendable/task.md deleted file mode 100644 index 244a932c..00000000 --- a/1-js/6-objects-more/3-constructor-new/4-calculator-extendable/task.md +++ /dev/null @@ -1,47 +0,0 @@ -# Создайте калькулятор - -[importance 5] - -Напишите конструктор `Calculator`, который создаёт расширяемые объекты-калькуляторы. - -Эта задача состоит из двух частей, которые можно решать одна за другой. -
        -
      1. Первый шаг задачи: вызов `calculate(str)` принимает строку, например "1 + 2", с жёстко заданным форматом "ЧИСЛО операция ЧИСЛО" (по одному пробелу вокруг операции), и возвращает результат. Понимает плюс `+` и минус `-`. - -Пример использования: - -```js -var calc = new Calculator; - -alert( calc.calculate("3 + 7") ); // 10 -``` - -
      2. -
      3. Второй шаг -- добавить калькулятору метод `addMethod(name, func)`, который учит калькулятор новой операции. Он получает имя операции `name` и функцию от двух аргументов `func(a,b)`, которая должна её реализовывать. - -Например, добавим операции умножить `*`, поделить `/` и возвести в степень `**`: - -```js -var powerCalc = new Calculator; -powerCalc.addMethod("*", function(a, b) { - return a * b; -}); -powerCalc.addMethod("/", function(a, b) { - return a / b; -}); -powerCalc.addMethod("**", function(a, b) { - return Math.pow(a, b); -}); - -var result = powerCalc.calculate("2 ** 3"); -alert( result ); // 8 -``` - -
      4. -
      - -
        -
      • Поддержка скобок и сложных математических выражений в этой задаче не требуется.
      • -
      • Числа и операции могут состоять из нескольких символов. Между ними ровно один пробел.
      • -
      • Предусмотрите обработку ошибок. Какая она должна быть - решите сами.
      • -
      \ No newline at end of file diff --git a/1-js/6-objects-more/3-constructor-new/article.md b/1-js/6-objects-more/3-constructor-new/article.md deleted file mode 100644 index f4ce62cf..00000000 --- a/1-js/6-objects-more/3-constructor-new/article.md +++ /dev/null @@ -1,216 +0,0 @@ -# Создание объектов через "new" - -Обычный синтаксис `{...}` позволяет создать один объект. Но зачастую нужно создать много однотипных объектов. - -Для этого используют "функции-конструкторы", запуская их при помощи специального оператора `new`. - -[cut] -## Конструктор - -Конструктором становится любая функция, вызванная через `new`. - -Например: - -```js -function Animal(name) { - this.name = name; - this.canWalk = true; -} - -*!* -var animal = new Animal("ёжик"); -*/!* -``` - -Заметим, что, технически, любая функция может быть использована как конструктор. То есть, любую функцию можно вызвать при помощи `new`. Как-то особым образом указывать, что она -- конструктор -- не надо. - -Но, чтобы выделить функции, задуманные как конструкторы, их называют с большой буквы: `Animal`, а не `animal`. - -Детальнее -- функция, запущенная через `new`, делает следующее: - -
        -
      1. Создаётся новый пустой объект.
      2. -
      3. Ключевое слово `this` получает ссылку на этот объект.
      4. -
      5. Функция выполняется. Как правило, она модифицирует `this`, добавляет методы, свойства.
      6. -
      7. Возвращается `this`.
      8. -
      - - -В результате вызова `new Animal("ёжик");` получаем такой объект: - -```js -animal = { - name: "ёжик", - canWalk: true -} -``` - -Иными словами, при вызове `new Animal` происходит что-то в таком духе (первая и последняя строка -- это то, что делает интерпретатор): - -```js -function Animal(name) { -*!* - // this = {}; -*/!* - - // в this пишем свойства, методы - this.name = name; - this.canWalk = true; - -*!* - // return this; -*/!* -} -``` - -Теперь многократными вызовами `new Animal` с разными параметрами мы можем создать столько объектов, сколько нужно. Поэтому такую функцию и называют *конструктором* -- она предназначена для "конструирования" объектов. - -[smart header="new function() { ... }"] -Иногда функцию-конструктор объявляют и тут же используют, вот так: -```js -var animal = new function() { - this.name = "Васька"; - this.canWalk = true; -}; -``` -Так делают, когда хотят создать единственный объект данного типа. Примере выше с тем же успехом можно было бы переписать как: -```js -var animal = { - name: "Васька", - canWalk: true -} -``` -...Но обычный синтаксис `{...}` не подходит, когда при создании свойств объекта нужны более сложные вычисления. Их можно проделать в функции-конструкторе и записать результат в `this`. -[/smart] - -## Правила обработки return - -Как правило, конструкторы ничего не возвращают. Их задача -- записать всё, что нужно, в `this`, который автоматически станет результатом. - -Но если явный вызов `return` всё же есть, то применяется простое правило: -
        -
      • При вызове `return` с объектом, будет возвращён он, а не `this`.
      • -
      • При вызове `return` с примитивным значением, оно будет отброшено.
      • -
      - -Иными словами, вызов `return` с объектом вернёт объект, а с чем угодно, кроме объекта -- возвратит, как обычно, `this`. - -Например, возврат объекта: - -```js -//+ run no-beautify -function BigAnimal() { - - this.name = "Мышь"; - - return { name: "Годзилла" }; // <-- возвратим объект -} - -alert( new BigAnimal().name ); // Годзилла, получили объект вместо this -``` - -А вот пример с возвратом строки: - -```js -//+ run -function BigAnimal() { - - this.name = "Мышь"; - - return "Годзилла"; // <-- возвратим примитив -} - -alert( new BigAnimal().name ); // Мышь, получили this (а Годзилла пропал) -``` - -Эта особенность работы `new` прописана в стандарте, но используется она весьма редко. - -[smart header="Можно без скобок"] -Кстати, при вызове `new` без аргументов скобки можно не ставить: - -```js -var animal = new BigAnimal; // <-- без скобок -// то же самое что -var animal = new BigAnimal(); -``` - -Не сказать, что выбрасывание скобок -- "хороший стиль", но такой синтаксис допустим стандартом. -[/smart] - -## Создание методов в конструкторе - -Использование функций для создания объекта дает большую гибкость. Можно передавать конструктору параметры, определяющие как его создавать, и он будет "клепать" объекты заданным образом. - -Добавим в создаваемый объект ещё и метод. - -Например, `new User(name)` создает объект с заданным значением свойства `name` и методом `sayHi`: - -```js -//+ run -function User(name) { - this.name = name; - - this.sayHi = function() { - alert( "Моё имя: " + this.name ); - }; -} - -*!* -var ivan = new User("Иван"); - -ivan.sayHi(); // Моё имя: Иван -*/!* - -/* -ivan = { - name: "Иван", - sayHi: функция -} -*/ -``` - -## Локальные переменные - -В функции-конструкторе бывает удобно объявить вспомогательные локальные переменные и вложенные функции, которые будут видны только внутри: - -```js -//+ run -function User(firstName, lastName) { -*!* - // вспомогательная переменная - var phrase = "Привет"; - - // вспомогательная вложенная функция - function getFullName() { - return firstName + " " + lastName; - } -*/!* - - this.sayHi = function() { - alert( phrase + ", " + getFullName() ); // использование - }; -} - -var vasya = new User("Вася", "Петров"); -vasya.sayHi(); // Привет, Вася Петров -``` - -Мы уже говорили об этом подходе ранее, в главе [](/closures-usage). - -Те функции и данные, которые должны быть доступны для внешнего кода, мы пишем в `this` -- и к ним можно будет обращаться, как например `vasya.sayHi()`, а вспомогательные, которые нужны только внутри самого объекта, сохраняем в локальной области видимости. - -[] - -## Итого - -Объекты могут быть созданы при помощи функций-конструкторов: - -
        -
      • Любая функция может быть вызвана с `new`, при этом она получает новый пустой объект в качестве `this`, в который она добавляет свойства. Если функция не решит возвратить свой объект, то её результатом будет `this`.
      • -
      • Функции, которые предназначены для создания объектов, называются *конструкторами*. Их названия пишут с большой буквы, чтобы отличать от обычных.
      • -
      - - - - - diff --git a/1-js/6-objects-more/4-descriptors-getters-setters/1-replace-property-getter/solution.md b/1-js/6-objects-more/4-descriptors-getters-setters/1-replace-property-getter/solution.md deleted file mode 100644 index f00b3bc8..00000000 --- a/1-js/6-objects-more/4-descriptors-getters-setters/1-replace-property-getter/solution.md +++ /dev/null @@ -1,48 +0,0 @@ - -```js -//+ run -function User(fullName) { - this.fullName = fullName; - - Object.defineProperties(this, { - - firstName: { - - get: function() { - return this.fullName.split(' ')[0]; - }, - - set: function(newFirstName) { - this.fullName = newFirstName + ' ' + this.lastName; - } - - }, - - lastName: { - - get: function() { - return this.fullName.split(' ')[1]; - }, - - set: function(newLastName) { - this.fullName = this.firstName + ' ' + newLastName; - } - - } - - - }); -} - -var vasya = new User("Василий Попкин"); - -// чтение firstName/lastName -alert( vasya.firstName ); // Василий -alert( vasya.lastName ); // Попкин - -// запись в lastName -vasya.lastName = 'Сидоров'; - -alert( vasya.fullName ); // Василий Сидоров -``` - diff --git a/1-js/6-objects-more/4-descriptors-getters-setters/1-replace-property-getter/task.md b/1-js/6-objects-more/4-descriptors-getters-setters/1-replace-property-getter/task.md deleted file mode 100644 index 81152200..00000000 --- a/1-js/6-objects-more/4-descriptors-getters-setters/1-replace-property-getter/task.md +++ /dev/null @@ -1,32 +0,0 @@ -# Добавить get/set-свойства - -[importance 5] - -Вам попал в руки код объекта `User`, который хранит имя и фамилию в свойстве `this.fullName`: - -```js -function User(fullName) { - this.fullName = fullName; -} - -var vasya = new User("Василий Попкин"); -``` - -Имя и фамилия всегда разделяются пробелом. - -Сделайте, чтобы были доступны свойства `firstName` и `lastName`, причём не только на чтение, но и на запись, вот так: - -```js -var vasya = new User("Василий Попкин"); - -// чтение firstName/lastName -alert( vasya.firstName ); // Василий -alert( vasya.lastName ); // Попкин - -// запись в lastName -vasya.lastName = 'Сидоров'; - -alert( vasya.fullName ); // Василий Сидоров -``` - -Важно: в этой задаче `fullName` должно остаться свойством, а `firstName/lastName` -- реализованы через `get/set`. Лишнее дублирование здесь ни к чему. \ No newline at end of file diff --git a/1-js/6-objects-more/4-descriptors-getters-setters/article.md b/1-js/6-objects-more/4-descriptors-getters-setters/article.md deleted file mode 100644 index 551b1f1e..00000000 --- a/1-js/6-objects-more/4-descriptors-getters-setters/article.md +++ /dev/null @@ -1,403 +0,0 @@ -# Дескрипторы, геттеры и сеттеры свойств - -В этой главе мы рассмотрим возможности, которые позволяют очень гибко и мощно управлять всеми свойствами объекта, включая их аспекты -- изменяемость, видимость в цикле `for..in` и даже незаметно делать их функциями. - -Они поддерживаются всеми современными браузерами, но не IE8-. Впрочем, даже в IE8 их поддерживает, но только для DOM-объектов (используются при работе со страницей, это сейчас вне нашего рассмотрения). - -[cut] -## Дескрипторы в примерах - -Основной метод для управления свойствами -- [Object.defineProperty](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperty). - -Он позволяет объявить свойство объекта и, что самое главное, тонко настроить его особые аспекты, которые никак иначе не изменить. - -Синтаксис: - -```js -Object.defineProperty(obj, prop, descriptor) -``` - -Аргументы: -
      -
      `obj`
      -
      Объект, в котором объявляется свойство.
      -
      `prop`
      -
      Имя свойства, которое нужно объявить или модифицировать.
      -
      `descriptor`
      -
      Дескриптор -- объект, который описывает поведение свойства. - -В нём могут быть следующие поля: - -
        -
      • `value` -- значение свойства, по умолчанию `undefined`
      • -
      • `writable` -- значение свойства можно менять, если `true`. По умолчанию `false`.
      • -
      • `configurable` -- если `true`, то свойство можно удалять, а также менять его в дальнейшем при помощи новых вызовов `defineProperty`. По умолчанию `false`.
      • -
      • `enumerable` -- если `true`, то свойство будет участвовать в переборе `for..in`. По умолчанию `false`.
      • -
      • `get` -- функция, которая возвращает значение свойства. По умолчанию `undefined`.
      • -
      • `set` -- функция, которая записывает значение свойства. По умолчанию `undefined`.
      • -
      - -Чтобы избежать конфликта, запрещено одновременно указывать значение `value` и функции `get/set`. Либо значение, либо функции для его чтения-записи, одно из двух. Также запрещено и не имеет смысла указывать `writable` при наличии `get/set`-функций. - -Далее мы подробно разберём эти свойства на примерах. - -## Обычное свойство - -Обычное свойство добавить очень просто. - -Два таких вызова работают одинаково: - -```js -//+ no-beautify -var user = {}; - -// 1. простое присваивание -user.name = "Вася"; - -// 2. указание значения через дескриптор -Object.defineProperty(user, "name", { value: "Вася" }); -``` - -## Свойство-константа - -Для того, чтобы сделать свойство неизменяемым, добавим ему флаги `writable` и `configurable`: - -```js -//+ run -*!* -"use strict"; -*/!* - -var user = {}; - -Object.defineProperty(user, "name", { - value: "Вася", - writable: false, // запретить присвоение "user.name=" - configurable: false // запретить удаление "delete user.name" -}); - -// Теперь попытаемся изменить это свойство. - -// в strict mode присвоение "user.name=" вызовет ошибку -*!* -user.name = "Петя"; -*/!* -``` - -Заметим, что без `use strict` операция записи "молча" не сработает, а при `use strict` дополнительно генерируется ошибка. - -## Свойство, скрытое для for..in - -Встроенный метод `toString`, как и большинство встроенных методов, не участвует в цикле `for..in`. Это удобно, так как обычно такое свойство является "служебным". - -К сожалению, свойство `toString`, объявленное обычным способом, будет видно в цикле `for..in`, например: - -```js -//+ run no-beautify -var user = { - name: "Вася", - toString: function() { return this.name; } -}; - -*!* -for(var key in user) alert(key); // name, toString -*/!* -``` - -Мы бы хотели, чтобы поведение нашего метода `toString` было таким же, как и стандартного. - -`Object.defineProperty` может исключить `toString` из списка итерации, поставив ему флаг `enumerable: false`. По стандарту, у встроенного `toString` этот флаг уже стоит. - -```js -//+ run no-beautify -var user = { - name: "Вася", - toString: function() { return this.name; } -}; - -*!* -// помечаем toString как не подлежащий перебору в for..in -Object.defineProperty(user, "toString", {enumerable: false}); - -for(var key in user) alert(key); // name -*/!* -``` - -Обратим внимание, вызов `defineProperty` не перезаписал свойство, а просто модифицировал настройки у существующего `toString`. - -## Свойство-функция - -Дескриптор позволяет задать свойство, которое на самом деле работает как функция. Для этого в нём нужно указать эту функцию в `get`. - -Например, у объекта `user` есть обычные свойства: имя `firstName` и фамилия `surname`. - -Создадим свойство `fullName`, которое на самом деле является функцией: - -```js -//+ run -var user = { - firstName: "Вася", - surname: "Петров" -} - -Object.defineProperty(user, "fullName", { - *!*get*/!*: function() { - return this.firstName + ' ' + this.surname; - } -}); - -*!* -alert(user.fullName); // Вася Петров -*/!* -``` - -Обратим внимание, снаружи `fullName` -- это обычное свойство `user.fullName`. Но дескриптор указывает, что на самом деле его значение возвращается функцией. - -Также можно указать функцию, которая используется для записи значения, при помощи дескриптора `set`. - -Например, добавим возможность присвоения `user.fullName` к примеру выше: - -```js -//+ run -var user = { - firstName: "Вася", - surname: "Петров" -} - -Object.defineProperty(user, "fullName", { - - get: function() { - return this.firstName + ' ' + this.surname; - }, - -*!* - set: function(value) { - var split = value.split(' '); - this.firstName = split[0]; - this.surname = split[1]; - } -*/!* -}); - -*!* -user.fullName = "Петя Иванов"; -*/!* -alert( user.firstName ); // Петя -alert( user.surname ); // Иванов -``` - -## Указание get/set в литералах - -Если мы создаём объект при помощи синтаксиса `{ ... }`, то задать свойства-функции можно прямо в его определении. - -Для этого используется особый синтаксис: `get свойство` или `set свойство`. - -Например, ниже объявлен геттер-сеттер `fullName`: - -```js -//+ run -var user = { - firstName: "Вася", - surname: "Петров", - -*!* - get fullName() { -*/!* - return this.firstName + ' ' + this.surname; - }, - -*!* - set fullName(value) { -*/!* - var split = value.split(' '); - this.firstName = split[0]; - this.surname = split[1]; - } -}; - -*!* -alert( user.fullName ); // Вася Петров (из геттера) - -user.fullName = "Петя Иванов"; -alert( user.firstName ); // Петя (поставил сеттер) -alert( user.surname ); // Иванов (поставил сеттер) -*/!* -``` - -## Да здравствуют get/set! - -Казалось бы, зачем нам назначать get/set для свойства через всякие хитрые вызовы, когда можно сделать просто функции с самого начала? Например, `getFullName`, `setFullName`... - -Конечно, в ряде случаев свойства выглядят короче, такое решение просто может быть красивым. Но основной бонус -- это гибкость, возможность получить контроль над свойством в любой момент! - -Например, в начале разработки мы используем обычные свойства, например у `User` будет имя `name` и возраст `age`: - -```js -function User(name, age) { - this.name = name; - this.age = age; -} - -var pete = new User("Петя", 25); - -alert( pete.age ); // 25 -``` - -С обычными свойствами в коде меньше букв, они удобны, причины использовать функции пока нет. - -...Но рано или поздно могут произойти изменения. Например, в `User` может стать более целесообразно вместо возраста `age` хранить дату рождения `birthday`: - -```js -function User(name, birthday) { - this.name = name; - this.birthday = birthday; -} - -var pete = new User("Петя", new Date(1987, 6, 1)); -``` - -Что теперь делать со старым кодом, который выводит свойство `age`? - -Можно, конечно, найти все места и поправить их, но это долго, а иногда и невозможно, скажем, если вы взаимодействуете со сторонней библиотекой, код в которой -- чужой и влезать в него нежелательно. - -Добавление `get`-функции `age` позволяет обойти проблему легко и непринуждённо: - -```js -//+ run no-beautify -function User(name, birthday) { - this.name = name; - this.birthday = birthday; - -*!* - // age будет высчитывать возраст по birthday - Object.defineProperty(this, "age", { - get: function() { - var todayYear = new Date().getFullYear(); - return todayYear - this.birthday.getFullYear(); - } - }); -*/!* -} - -var pete = new User("Петя", new Date(1987, 6, 1)); - -alert( pete.birthday ); // и дата рождения доступна -alert( pete.age ); // и возраст -``` - -Заметим, что `pete.age` снаружи как было свойством, так и осталось. То есть, переписывать внешний код на вызов функции `pete.age()` не нужно. - -Таким образом, `defineProperty` позволяет нам начать с обычных свойств, а в будущем, при необходимости, можно в любой момент заменить их на функции, реализующие более сложную логику. - -## Другие методы работы со свойствами - -
      -
      [Object.defineProperties(obj, descriptors)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperties)
      -
      Позволяет объявить несколько свойств сразу: - -```js -//+ run -var user = {} - -Object.defineProperties(user, { -*!* - firstName: { -*/!* - value: "Петя" - }, - -*!* - surname: { -*/!* - value: "Иванов" - }, - -*!* - fullName: { -*/!* - get: function() { - return this.firstName + ' ' + this.surname; - } - } -}); - -alert( user.fullName ); // Петя Иванов -``` - -
      -
      [Object.keys(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys), [Object.getOwnPropertyNames(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames)
      -
      Возвращают массив -- список свойств объекта. - -`Object.keys` возвращает только `enumerable`-свойства. - -`Object.getOwnPropertyNames` -- возвращает все: - -```js -//+ run -var obj = { - a: 1, - b: 2, - internal: 3 -}; - -Object.defineProperty(obj, "internal", { - enumerable: false -}); - -*!* -alert( Object.keys(obj) ); // a,b -alert( Object.getOwnPropertyNames(obj) ); // a, internal, b -*/!* -``` - -
      -
      [Object.getOwnPropertyDescriptor(obj, prop)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor)
      -
      Возвращает дескриптор для свойства `obj[prop]`. - -Полученный дескриптор можно изменить и использовать `defineProperty` для сохранения изменений, например: - -```js -//+ run -var obj = { - test: 5 -}; -*!* -var descriptor = Object.getOwnPropertyDescriptor(obj, 'test'); -*/!* - -*!* -// заменим value на геттер, для этого... -*/!* -delete descriptor.value; // ..нужно убрать value/writable -delete descriptor.writable; -descriptor.get = function() { // и поставить get - alert( "Preved :)" ); -}; - -*!* -// поставим новое свойство вместо старого -*/!* - -// если не удалить - defineProperty объединит старый дескриптор с новым -delete obj.test; - -Object.defineProperty(obj, 'test', descriptor); - -obj.test; // Preved :) -``` - -
      -
      - -...И несколько методов, которые используются очень редко: -
      -
      [Object.preventExtensions(obj)](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions)
      -
      Запрещает добавление свойств в объект.
      -
      [Object.seal(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/seal)
      -
      Запрещает добавление и удаление свойств, все текущие свойства делает `configurable: false`.
      -
      [Object.freeze(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/freeze)
      -
      Запрещает добавление, удаление и изменение свойств, все текущие свойства делает `configurable: false, writable: false`.
      -
      [Object.isExtensible(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isExtensible), [Object.isSealed(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isSealed), [Object.isFrozen(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isFrozen)
      -
      Возвращают `true`, если на объекте были вызваны методы `Object.preventExtensions/seal/freeze`.
      -
      - diff --git a/1-js/6-objects-more/5-static-properties-and-methods/1-objects-counter/_js.view/solution.js b/1-js/6-objects-more/5-static-properties-and-methods/1-objects-counter/_js.view/solution.js deleted file mode 100644 index 03b48506..00000000 --- a/1-js/6-objects-more/5-static-properties-and-methods/1-objects-counter/_js.view/solution.js +++ /dev/null @@ -1,11 +0,0 @@ -function Article() { - this.created = new Date; - - Article.count++; // увеличиваем счетчик при каждом вызове - Article.last = this.created; // и запоминаем дату -} -Article.count = 0; // начальное значение - -Article.showStats = function() { - alert('Всего: ' + this.count + ', Последняя: ' + this.last); -}; \ No newline at end of file diff --git a/1-js/6-objects-more/5-static-properties-and-methods/1-objects-counter/_js.view/test.js b/1-js/6-objects-more/5-static-properties-and-methods/1-objects-counter/_js.view/test.js deleted file mode 100644 index 7bf37039..00000000 --- a/1-js/6-objects-more/5-static-properties-and-methods/1-objects-counter/_js.view/test.js +++ /dev/null @@ -1,28 +0,0 @@ -describe("Article.showStats", function() { - before(function() { - sinon.stub(window, "alert"); - this.clock = sinon.useFakeTimers(); - }); - - after(function() { - window.alert.restore(); - this.clock.restore(); - }); - - it("Выводит число статей и дату создания последней", function() { - new Article(); - this.clock.tick(100); - new Article(); - Article.showStats(); - - assert(alert.calledWith('Всего: 2, Последняя: ' + new Date())); - }); - - it("и ещё одна статья...", function() { - this.clock.tick(100); - new Article(); - Article.showStats(); - - assert(alert.calledWith('Всего: 3, Последняя: ' + new Date())); - }); -}); \ No newline at end of file diff --git a/1-js/6-objects-more/5-static-properties-and-methods/1-objects-counter/solution.md b/1-js/6-objects-more/5-static-properties-and-methods/1-objects-counter/solution.md deleted file mode 100644 index 45ce7d39..00000000 --- a/1-js/6-objects-more/5-static-properties-and-methods/1-objects-counter/solution.md +++ /dev/null @@ -1,29 +0,0 @@ -Решение (как вариант): - -```js -//+ run -function Article() { - this.created = new Date; - -*!* - Article.count++; // увеличиваем счетчик при каждом вызове - Article.last = this.created; // и запоминаем дату -*/!* -} -Article.count = 0; // начальное значение -// (нельзя оставить undefined, т.к. Article.count++ будет NaN) - -Article.showStats = function() { - alert( 'Всего: ' + this.count + ', Последняя: ' + this.last ); -}; - -new Article(); -new Article(); - -Article.showStats(); // Всего: 2, Последняя: (дата) - -new Article(); - -Article.showStats(); // Всего: 3, Последняя: (дата) -``` - diff --git a/1-js/6-objects-more/5-static-properties-and-methods/1-objects-counter/task.md b/1-js/6-objects-more/5-static-properties-and-methods/1-objects-counter/task.md deleted file mode 100644 index 513ed072..00000000 --- a/1-js/6-objects-more/5-static-properties-and-methods/1-objects-counter/task.md +++ /dev/null @@ -1,33 +0,0 @@ -# Счетчик объектов - -[importance 5] - -Добавить в конструктор `Article`: -
        -
      • Подсчёт общего количества созданных объектов.
      • -
      • Запоминание даты последнего созданного объекта.
      • -
      -Используйте для этого статические свойства. - -Пусть вызов `Article.showStats()` выводит то и другое. - -Использование: - -```js -function Article() { - this.created = new Date(); -*!* - // ... ваш код ... -*/!* -} - -new Article(); -new Article(); - -Article.showStats(); // Всего: 2, Последняя: (дата) - -new Article(); - -Article.showStats(); // Всего: 3, Последняя: (дата) -``` - diff --git a/1-js/6-objects-more/5-static-properties-and-methods/article.md b/1-js/6-objects-more/5-static-properties-and-methods/article.md deleted file mode 100644 index 3595a734..00000000 --- a/1-js/6-objects-more/5-static-properties-and-methods/article.md +++ /dev/null @@ -1,255 +0,0 @@ -# Статические и фабричные методы - -Методы и свойства, которые не привязаны к конкретному экземпляру объекта, называют "статическими". Их записывают прямо в саму функцию-конструктор. - -[cut] - -## Статические свойства - -В коде ниже используются статические свойства `Article.count` и `Article.DEFAULT_FORMAT`: - -```js -function Article() { - Article.count++; -} - -Article.count = 0; // статическое свойство-переменная -Article.DEFAULT_FORMAT = "html"; // статическое свойство-константа -``` - -Они хранят данные, специфичные не для одного объекта, а для всех статей целиком. - -Как правило, это чаще константы, такие как формат "по умолчанию" `Article.DEFAULT_FORMAT`. - -## Статические методы - -С примерами статических методов мы уже знакомы: это встроенные методы [String.fromCharCode](http://javascript.ru/String.fromCharCode), [Date.parse](http://javascript.ru/Date.parse). - -Создадим для `Article` статический метод `Article.showCount()`: - -```js -//+ run -function Article() { - Article.count++; - - //... -} -Article.count = 0; - -Article.showCount = function() { -*!* - alert( this.count ); // (1) -*/!* -} - -// использование -new Article(); -new Article(); -Article.showCount(); // (2) -``` - -Здесь `Article.count` -- статическое свойство, а `Article.showCount` -- статический метод. - -Обратим внимание на использование `this` в примере выше. Несмотря на то, что переменная и метод -- статические, он всё ещё полезен. В строке `(1)` он равен `Article`. - -## Пример: сравнение объектов - -Ещё один хороший способ применения -- сравнение объектов. - -Например, у нас есть объект `Journal` для журналов. Журналы можно сравнивать -- по толщине, по весу, по другим параметрам. - -Объявим "стандартную" функцию сравнения, которая будет сравнивать по дате издания. Эта функция сравнения, естественно, не привязана к конкретному журналу, но относится к журналам вообще. - -Поэтому зададим её как статический метод `Journal.compare`: - -```js -function Journal(date) { - this.date = date; - // ... -} - -// возвращает значение, большее 0, если A больше B, иначе меньшее 0 -Journal.compare = function(journalA, journalB) { - return journalA.date - journalB.date; -}; -``` - -В примере ниже эта функция используется для поиска самого раннего журнала из массива: - -```js -//+ run -function Journal(date) { - this.date = date; - - this.formatDate = function(date) { - return date.getDate() + '.' + (date.getMonth() + 1) + '.' + date.getFullYear(); - }; - - this.getTitle = function() { - return "Выпуск от " + this.formatDate(this.date); - }; - -} - -*!* -Journal.compare = function(journalA, journalB) { - return journalA.date - journalB.date; -}; -*/!* - -// использование: -var journals = [ - new Journal(new Date(2012, 1, 1)), - new Journal(new Date(2012, 0, 1)), - new Journal(new Date(2011, 11, 1)) -]; - -function findMin(journals) { - var min = 0; - for (var i = 0; i < journals.length; i++) { -*!* - // используем статический метод - if (Journal.compare(journals[min], journals[i]) > 0) min = i; -*/!* - } - return journals[min]; -} - -alert( findMin(journals).getTitle() ); -``` - -**Статический метод также можно использовать для функций, которые вообще не требуют наличия объекта.** - -Например, метод `formatDate(date)` можно сделать статическим. Он будет форматировать дату "как это принято в журналах", при этом его можно использовать в любом месте кода, не обязательно создавать журнал. - -Например: - -```js -//+ run -function Journal() { /*...*/ } - -Journal.formatDate = function(date) { - return date.getDate() + '.' + (date.getMonth()+1) + '.' + date.getFullYear(); -} - -// ни одного объекта Journal нет, просто форматируем дату -alert( *!*Journal.formatDate(new Date)*/!* ); -``` - -## Фабричные методы - -Рассмотрим ситуацию, когда объект нужно создавать различными способами. Например, это реализовано во встроенном объекте [Date](/datetime). Он по-разному обрабатывает аргументы разных типов: - -
        -
      • `new Date()` -- создаёт объект с текущей датой,
      • -
      • `new Date(milliseconds)` -- создаёт дату по количеству миллисекунд `milliseconds`,
      • -
      • `new Date(year, month, day ...)` -- создаёт дату по компонентам год, месяц, день...
      • -
      • `new Date(datestring)` -- читает дату из строки `datestring`
      • -
      - -**"Фабричный статический метод" -- удобная альтернатива такому конструктору. Так называется статический метод, который служит для создания новых объектов (поэтому и называется "фабричным").** - -Пример встроенного фабричного метода -- [String.fromCharCode(code)](http://javascript.ru/String.fromCharCode). Этот метод создает строку из кода символа: - -```js -//+ run -var str = String.fromCharCode(65); -alert( str ); // 'A' -``` - -Но строки -- слишком простой пример, посмотрим что-нибудь посложнее. - -Допустим, нам нужно создавать объекты `User`: анонимные `new User()` и с данными `new User({name: 'Вася', age: 25})`. - -Можно, конечно, создать полиморфную функцию-конструктор `User`: - -```js -//+ run -function User(userData) { - if (userData) { // если указаны данные -- одна ветка if - this.name = userData.name; - this.age = userData.age; - } else { // если не указаны -- другая - this.name = 'Аноним'; - } - - this.sayHi = function() { - alert(this.name) - }; - // ... -} - -// Использование - -var guest = new User(); -guest.sayHi(); // Аноним - -var knownUser = new User({ - name: 'Вася', - age: 25 -}); -knownUser.sayHi(); // Вася -``` - -Подход с использованием фабричных методов был бы другим. Вместо разбора параметров в конструкторе -- делаем два метода: `User.createAnonymous` и `User.createFromData`. - -Код: - -```js -//+ run -function User() { - this.sayHi = function() { - alert(this.name) - }; -} - -User.createAnonymous = function() { - var user = new User; - user.name = 'Аноним'; - return user; -} - -User.createFromData = function(userData) { - var user = new User; - user.name = userData.name; - user.age = userData.age; - return user; -} - -// Использование - -*!* -var guest = User.createAnonymous(); -guest.sayHi(); // Аноним - -var knownUser = User.createFromData({ - name: 'Вася', - age: 25 -}); -knownUser.sayHi(); // Вася -*/!* -``` - -Преимущества использования фабричных методов: - -[compare] -+Лучшая читаемость кода. Как конструктора -- вместо одной большой функции несколько маленьких, так и вызывающего кода -- явно видно, что именно создаётся. -+Лучший контроль ошибок, т.к. если в `createFromData` ничего не передали, то будет ошибка, а полиморфный конструктор создал бы анонимного посетителя. -+Удобная расширяемость. Например, нужно добавить создание администратора, без аргументов. Фабричный метод сделать легко: `User.createAdmin = function() { ... }`. А для полиморфного конструктора вызов без аргумента создаст анонима, так что нужно добавить параметр -- "тип посетителя" и усложнить этим код. -[/compare] - -**Поэтому полиморфные конструкторы лучше использовать там, где нужна именно полиморфность**, т.е. когда непонятно, какого типа аргумент передадут, и хочется в одном конструкторе охватить все варианты. - -А в остальных случаях отличная альтернатива -- фабричные методы. - -## Итого - -Статические свойства и методы объекта удобно применять в следующих случаях: - -
        -
      • Общие действия и подсчёты, имеющие отношения ко всем объектам данного типа. В примерах выше это подсчёт количества.
      • -
      • Методы, не привязанные к конкретному объекту, например сравнение.
      • -
      • Вспомогательные методы, которые полезны вне объекта, например для форматирования даты.
      • -
      • Фабричные методы.
      • -
      - diff --git a/1-js/6-objects-more/6-call-apply/1-rewrite-sum-arguments/solution.md b/1-js/6-objects-more/6-call-apply/1-rewrite-sum-arguments/solution.md deleted file mode 100644 index 9e5ca007..00000000 --- a/1-js/6-objects-more/6-call-apply/1-rewrite-sum-arguments/solution.md +++ /dev/null @@ -1,31 +0,0 @@ -# Первый вариант - -```js -//+ run -function sumArgs() { - // скопируем reduce из массива - arguments.reduce = [].reduce; - return arguments.reduce(function(a, b) { - return a + b; - }); -} - -alert( sumArgs(4, 5, 6) ); // 15 -``` - -# Второй вариант - -Метод `call` здесь вполне подойдёт, так как требуется вызвать `reduce` в контексте `arguments` с одним аргументом. - -```js -//+ run -function sumArgs() { - // запустим reduce из массива напрямую - return [].reduce.call(arguments, function(a, b) { - return a + b; - }); -} - -alert( sumArgs(4, 5, 6) ); // 15 -``` - diff --git a/1-js/6-objects-more/6-call-apply/1-rewrite-sum-arguments/task.md b/1-js/6-objects-more/6-call-apply/1-rewrite-sum-arguments/task.md deleted file mode 100644 index b03a9fc7..00000000 --- a/1-js/6-objects-more/6-call-apply/1-rewrite-sum-arguments/task.md +++ /dev/null @@ -1,30 +0,0 @@ -# Перепишите суммирование аргументов - -[importance 5] - -Есть функция `sum`, которая суммирует все элементы массива: - -```js -//+ run -function sum(arr) { - return arr.reduce(function(a, b) { - return a + b; - }); -} - -alert( sum([1, 2, 3]) ); // 6 (=1+2+3) -``` - -Создайте аналогичную функцию `sumArgs()`, которая будет суммировать все свои аргументы: - -```js -function sumArgs() { - /* ваш код */ -} - -alert( sumArgs(1, 2, 3) ); // 6, аргументы переданы через запятую, без массива -``` - -Для решения примените метод `reduce` к `arguments`, используя `call`, `apply` или одалживание метода. - -P.S. Функция `sum` вам не понадобится, она приведена в качестве примера использования `reduce` для похожей задачи. \ No newline at end of file diff --git a/1-js/6-objects-more/6-call-apply/2-apply-function-skip-first-argument/_js.view/solution.js b/1-js/6-objects-more/6-call-apply/2-apply-function-skip-first-argument/_js.view/solution.js deleted file mode 100644 index b49aeb62..00000000 --- a/1-js/6-objects-more/6-call-apply/2-apply-function-skip-first-argument/_js.view/solution.js +++ /dev/null @@ -1,3 +0,0 @@ -function applyAll(func) { - return func.apply(this, [].slice.call(arguments, 1)); -} \ No newline at end of file diff --git a/1-js/6-objects-more/6-call-apply/2-apply-function-skip-first-argument/_js.view/test.js b/1-js/6-objects-more/6-call-apply/2-apply-function-skip-first-argument/_js.view/test.js deleted file mode 100644 index 871a19a7..00000000 --- a/1-js/6-objects-more/6-call-apply/2-apply-function-skip-first-argument/_js.view/test.js +++ /dev/null @@ -1,15 +0,0 @@ -describe("applyAll", function() { - - it("применяет функцию ко всем аргументам, начиная со 2го", function() { - var min = applyAll(Math.min, 1, 2, 3); - assert.equal(min, 1); - }); - - it("при отсутствии аргументов просто вызывает функцию", function() { - var spy = sinon.spy(); - applyAll(spy); - assert(spy.calledOnce); - assert.equal(spy.firstCall.args.length, 0); - }); - -}); \ No newline at end of file diff --git a/1-js/6-objects-more/6-call-apply/2-apply-function-skip-first-argument/solution.md b/1-js/6-objects-more/6-call-apply/2-apply-function-skip-first-argument/solution.md deleted file mode 100644 index c8579b0a..00000000 --- a/1-js/6-objects-more/6-call-apply/2-apply-function-skip-first-argument/solution.md +++ /dev/null @@ -1,28 +0,0 @@ - - -```js -//+ run -function sum() { - return [].reduce.call(arguments, function(a, b) { - return a + b; - }); -} - -function mul() { - return [].reduce.call(arguments, function(a, b) { - return a * b; - }); -} - -*!* -function applyAll(func) { - return func.apply(this, [].slice.call(arguments, 1)); - } -*/!* - -alert( applyAll(sum, 1, 2, 3) ); // 6 -alert( applyAll(mul, 2, 3, 4) ); // 24 -alert( applyAll(Math.max, 2, -2, 3) ); // 3 -alert( applyAll(Math.min, 2, -2, 3) ); // -2 -``` - diff --git a/1-js/6-objects-more/6-call-apply/2-apply-function-skip-first-argument/task.md b/1-js/6-objects-more/6-call-apply/2-apply-function-skip-first-argument/task.md deleted file mode 100644 index 95bd2413..00000000 --- a/1-js/6-objects-more/6-call-apply/2-apply-function-skip-first-argument/task.md +++ /dev/null @@ -1,40 +0,0 @@ -# Примените функцию к аргументам - -[importance 5] - -Напишите функцию `applyAll(func, arg1, arg2...)`, которая получает функцию `func` и произвольное количество аргументов. - -Она должна вызвать `func(arg1, arg2...)`, то есть передать в `func` все аргументы, начиная со второго, и возвратить результат. - -Например: - -```js -// Применить Math.max к аргументам 2, -2, 3 -alert( applyAll(Math.max, 2, -2, 3) ); // 3 - -// Применить Math.min к аргументам 2, -2, 3 -alert( applyAll(Math.min, 2, -2, 3) ); // -2 -``` - -Область применения `applyAll`, конечно, шире, можно вызывать её и со своими функциями: - -```js -//+ run -function sum() { // суммирует аргументы: sum(1,2,3) = 6 - return [].reduce.call(arguments, function(a, b) { - return a + b; - }); -} - -function mul() { // перемножает аргументы: mul(2,3,4) = 24 - return [].reduce.call(arguments, function(a, b) { - return a * b; - }); -} - -*!* -alert( applyAll(sum, 1, 2, 3) ); // -> sum(1, 2, 3) = 6 -alert( applyAll(mul, 2, 3, 4) ); // -> mul(2, 3, 4) = 24 -*/!* -``` - diff --git a/1-js/6-objects-more/6-call-apply/article.md b/1-js/6-objects-more/6-call-apply/article.md deleted file mode 100644 index 08f9f386..00000000 --- a/1-js/6-objects-more/6-call-apply/article.md +++ /dev/null @@ -1,311 +0,0 @@ -# Явное указание this: "call", "apply" - -Итак, мы знаем, что `this` -- это текущий объект при вызове "через точку" и новый объект при конструировании через `new`. - -В этой главе наша цель получить окончательное и полное понимание `this` в JavaScript. Для этого не хватает всего одного элемента: способа явно указать `this` при помощи методов `call` и `apply`. - -[cut] - -## Метод call - -Синтаксис метода `call`: - -```js -func.call(context, arg1, arg2, ...) -``` - -При этом вызывается функция `func`, первый аргумент `call` становится её `this`, а остальные передаются "как есть". - -**Вызов `func.call(context, a, b...)` -- то же, что обычный вызов `func(a, b...)`, но с явно указанным `this(=context)`.** - -Например, у нас есть функция `showFullName`, которая работает с `this`: - -```js -function showFullName() { - alert( this.firstName + " " + this.lastName ); -} -``` - -Пока объекта нет, но это нормально, ведь JavaScript позволяет использовать `this` везде. Любая функция может в своём коде упомянуть `this`, каким будет это значение -- выяснится в момент запуска. - -Вызов `showFullName.call(user)` запустит функцию, установив `this = user`, вот так: - -```js -//+ run -function showFullName() { - alert( this.firstName + " " + this.lastName ); -} - -var user = { - firstName: "Василий", - lastName: "Петров" -}; - -*!* -// функция вызовется с this=user -showFullName.call(user) // "Василий Петров" -*/!* -``` - -После контекста в `call` можно передать аргументы для функции. Вот пример с более сложным вариантом `showFullName`, который конструирует ответ из указанных свойств объекта: - -```js -//+ run -var user = { - firstName: "Василий", - surname: "Петров", - patronym: "Иванович" -}; - -function showFullName(firstPart, lastPart) { - alert( this[firstPart] + " " + this[lastPart] ); -} - -*!* -// f.call(контекст, аргумент1, аргумент2, ...) -showFullName.call(user, 'firstName', 'surname') // "Василий Петров" -showFullName.call(user, 'firstName', 'patronym') // "Василий Иванович" -*/!* -``` - -## "Одалживание метода" - -При помощи `call` можно легко взять метод одного объекта, в том числе встроенного, и вызвать в контексте другого. - -Это называется "одалживание метода" (на англ. *method borrowing*). - -**Используем эту технику для упрощения манипуляций с `arguments`.** - -Как мы знаем, `arguments` не массив, а обычный объект, поэтому таких полезных методов как `push`, `pop`, `join` и других у него нет. Но иногда так хочется, чтобы были... - -Нет ничего проще! Давайте скопируем метод `join` из обычного массива: - -```js -//+ run -function printArgs() { - arguments.join = [].join; // одолжили метод (1) - - var argStr = arguments.join(':'); // (2) - - alert( argStr ); // сработает и выведет 1:2:3 -} - -printArgs(1, 2, 3); -``` - -
        -
      1. В строке `(1)` объявлен пустой массив `[]` и скопирован его метод `[].join`. Обратим внимание, мы не вызываем его, а просто копируем. Функция, в том числе встроенная -- обычное значение, мы можем скопировать любое свойство любого объекта, и `[].join` здесь не исключение.
      2. -
      3. В строке `(2)` запустили `join` в контексте `arguments`, как будто он всегда там был.
      4. - -[smart header="Почему вызов сработает?"] - -Здесь метод join массива скопирован и вызван в контексте `arguments`. Не произойдёт ли что-то плохое от того, что `arguments` -- не массив? Почему он, вообще, сработал? - -Ответ на эти вопросы простой. В соответствии [со спецификацией](http://es5.github.com/x15.4.html#x15.4.4.5), внутри `join` реализован примерно так: - -```js -function join(separator) { - if (!this.length) return ''; - - var str = this[0]; - - for (var i = 1; i < this.length; i++) { - str += separator + this[i]; - } - - return str; -} -``` - -Как видно, используется `this`, числовые индексы и свойство `length`. Если эти свойства есть, то все в порядке. А больше ничего и не нужно. - -В качестве `this` подойдёт даже обычный объект: - -```js -//+ run -var obj = { // обычный объект с числовыми индексами и length - 0: "А", - 1: "Б", - 2: "В", - length: 3 -}; - -*!* -obj.join = [].join; -alert( obj.join(';') ); // "A;Б;В" -*/!* -``` - -[/smart] - -...Однако, копирование метода из одного объекта в другой не всегда приемлемо! - -Представим на минуту, что вместо `arguments` у нас -- произвольный объект. У него тоже есть числовые индексы, `length` и мы хотим вызвать в его контексте метод `[].join`. То есть, ситуация похожа на `arguments`, но (!) вполне возможно, что у объекта есть *свой* метод `join`. - -Поэтому копировать `[].join`, как сделано выше, нельзя: если он перезапишет собственный `join` объекта, то будет страшный бардак и путаница. - -Безопасно вызвать метод нам поможет `call`: - -```js -//+ run -function printArgs() { - var join = [].join; // скопируем ссылку на функцию в переменную - -*!* - // вызовем join с this=arguments, - // этот вызов эквивалентен arguments.join(':') из примера выше - var argStr = join.call(arguments, ':'); -*/!* - - alert( argStr ); // сработает и выведет 1:2:3 -} - -printArgs(1, 2, 3); -``` - -Мы вызвали метод без копирования. Чисто, безопасно. - -## Ещё пример: [].slice.call(arguments) - -В JavaScript есть очень простой способ сделать из `arguments` настоящий массив. Для этого возьмём метод массива: slice. - -По стандарту вызов `arr.slice(start, end)` создаёт новый массив и копирует в него элементы массива `arr` от `start` до `end`. А если `start` и `end` не указаны, то копирует весь массив. - -Вызовем его в контексте `arguments`: - -```js -//+ run -function printArgs() { - // вызов arr.slice() скопирует все элементы из this в новый массив -*!* - var args = [].slice.call(arguments); -*/!* - alert( args.join(', ') ); // args - полноценный массив из аргументов -} - -printArgs('Привет', 'мой', 'мир'); // Привет, мой, мир -``` - -Как и в случае с `join`, такой вызов технически возможен потому, что `slice` для работы требует только нумерованные свойства и `length`. Всё это в `arguments` есть. - -## Метод apply - -Если нам неизвестно, с каким количеством аргументов понадобится вызвать функцию, можно использовать более мощный метод: `apply`. - -**Вызов функции при помощи `func.apply` работает аналогично `func.call`, но принимает массив аргументов вместо списка.** - -```js -func.call(context, arg1, arg2); -// идентичен вызову -func.apply(context, [arg1, arg2]); -``` - -В частности, эти две строчки cработают одинаково: - -```js -showFullName.call(user, 'firstName', 'surname'); - -showFullName.apply(user, ['firstName', 'surname']); -``` - -Преимущество `apply` перед `call` отчётливо видно, когда мы формируем массив аргументов динамически. - -Например, в JavaScript есть встроенная функция `Math.max(a, b, c...)`, которая возвращает максимальное значение из аргументов: - -```js -//+ run -alert( Math.max(1, 5, 2) ); // 5 -``` - -При помощи `apply` мы могли бы найти максимум в произвольном массиве, вот так: - -```js -//+ run -var arr = []; -arr.push(1); -arr.push(5); -arr.push(2); - -// получить максимум из элементов arr -alert( Math.max.apply(null, arr) ); // 5 -``` - -В примере выше мы передали аргументы через массив -- второй параметр `apply`... Но вы, наверное, заметили небольшую странность? В качестве контекста `this` был передан `null`. - -Строго говоря, полным эквивалентом вызову `Math.max(1,2,3)` был бы вызов `Math.max.apply(Math, [1,2,3])`. В обоих этих вызовах контекстом будет объект `Math`. - -Но в данном случае в качестве контекста можно передавать что угодно, поскольку в своей внутренней реализации метод `Math.max` не использует `this`. Действительно, зачем `this`, если нужно всего лишь выбрать максимальный из аргументов? Вот так, при помощи `apply` мы получили короткий и элегантный способ вычислить максимальное значение в массиве! - -[smart header="Вызов `call/apply` с `null` или `undefined`"] - -В современном стандарте `call/apply` передают `this` "как есть". А в старом, без `use strict`, при указании первого аргумента `null` или `undefined` в `call/apply`, функция получает `this = window`, например: - -Современный стандарт: -```js -//+ run -function f() { - "use strict"; -*!* - alert( this ); // null -*/!* -} - -f.call(null); -``` - -Без `use strict`: - -```js -//+ run -function f() { - alert( this ); // window -} - -f.call(null); -``` - -[/smart] - -## Итого про this - -Значение `this` устанавливается в зависимости от того, как вызвана функция: - -
        -
        При вызове функции как метода
        -
        - -```js -//+ no-beautify -obj.func(...) // this = obj -obj["func"](...) -``` - -
        -
        При обычном вызове
        -
        - -```js -func(...) // this = window (ES3) /undefined (ES5) -``` - -
        -
        В `new`
        -
        - -```js -new func() // this = {} (новый объект) -``` - -
        -
        Явное указание
        -
        - -```js -func.apply(context, args) // this = context (явная передача) -func.call(context, arg1, arg2, ...) -``` - -
        -
        - - diff --git a/1-js/6-objects-more/7-bind/1-cross-browser-bind/solution.md b/1-js/6-objects-more/7-bind/1-cross-browser-bind/solution.md deleted file mode 100644 index 28b52dcb..00000000 --- a/1-js/6-objects-more/7-bind/1-cross-browser-bind/solution.md +++ /dev/null @@ -1,8 +0,0 @@ - -Страшновато выглядит, да? Работает так (по строкам): -
          -
        1. Вызов `bind` сохраняет дополнительные аргументы `args` (они идут со 2го номера) в массив `bindArgs`.
        2. -
        3. ... и возвращает обертку `wrapper`.
        4. -
        5. Эта обёртка делает из `arguments` массив `args` и затем, используя метод [concat](http://javascript.ru/Array/concat), прибавляет их к аргументам `bindArgs` (карринг).
        6. -
        7. Затем передаёт вызов `func` с контекстом и общим массивом аргументов.
        8. -
        diff --git a/1-js/6-objects-more/7-bind/1-cross-browser-bind/task.md b/1-js/6-objects-more/7-bind/1-cross-browser-bind/task.md deleted file mode 100644 index ce481712..00000000 --- a/1-js/6-objects-more/7-bind/1-cross-browser-bind/task.md +++ /dev/null @@ -1,23 +0,0 @@ -# Кросс-браузерная эмуляция bind - -[importance 3] - -Если вы вдруг захотите копнуть поглубже -- аналог `bind` для IE8- и старых версий других браузеров будет выглядеть следующим образом: - -```js -//+ no-beautify -function bind(func, context /*, args*/) { - var bindArgs = [].slice.call(arguments, 2); // (1) - function wrapper() { // (2) - var args = [].slice.call(arguments); - var unshiftArgs = bindArgs.concat(args); // (3) - return func.apply(context, unshiftArgs); // (4) - } - return wrapper; -} -``` - -Использование -- вместо `mul.bind(null, 2)` вызывать `bind(mul, null, 2)`. - -Не факт, что он вам понадобится, но в качестве упражнение попробуйте разобраться, как это работает. - diff --git a/1-js/6-objects-more/7-bind/2-write-to-object-after-bind/solution.md b/1-js/6-objects-more/7-bind/2-write-to-object-after-bind/solution.md deleted file mode 100644 index ccd6ca0c..00000000 --- a/1-js/6-objects-more/7-bind/2-write-to-object-after-bind/solution.md +++ /dev/null @@ -1,22 +0,0 @@ -Ответ: `Hello`. - -```js -//+ run -function f() { - alert( this ); -} - -var user = { - g: f.bind("Hello") -} - -user.g(); -``` - -Так как вызов идёт в контексте объекта `user.g()`, то внутри функции `g` контекст `this = user`. - -Однако, функции `g` совершенно без разницы, какой `this` она получила. - -Её единственное предназначение -- это передать вызов в `f` вместе с аргументами и ранее указанным контекстом `"Hello"`, что она и делает. - -Эта задача демонстрирует, что изменить однажды привязанный контекст уже нельзя. \ No newline at end of file diff --git a/1-js/6-objects-more/7-bind/2-write-to-object-after-bind/task.md b/1-js/6-objects-more/7-bind/2-write-to-object-after-bind/task.md deleted file mode 100644 index a189eeb3..00000000 --- a/1-js/6-objects-more/7-bind/2-write-to-object-after-bind/task.md +++ /dev/null @@ -1,18 +0,0 @@ -# Запись в объект после bind - -[importance 5] - -Что выведет функция? - -```js -function f() { - alert( this ); -} - -var user = { - g: f.bind("Hello") -} - -user.g(); -``` - diff --git a/1-js/6-objects-more/7-bind/3-second-bind/solution.md b/1-js/6-objects-more/7-bind/3-second-bind/solution.md deleted file mode 100644 index 2a8b26d8..00000000 --- a/1-js/6-objects-more/7-bind/3-second-bind/solution.md +++ /dev/null @@ -1,58 +0,0 @@ -Ответ: `"Вася"`. - -```js -//+ run no-beautify -function f() { - alert(this.name); -} - -f = f.bind( {name: "Вася"} ).bind( {name: "Петя"} ); - -f(); // Вася -``` - -Первый вызов `f.bind(..Вася..)` возвращает "обёртку", которая устанавливает контекст для `f` и передаёт вызов `f`. - -Следующий вызов `bind` будет устанавливать контекст уже для этой обёртки. Это ни на что не повлияет. - -Чтобы это проще понять, используем наш собственный вариант `bind` вместо встроенного: - -```js -function bind(func, context) { - return function() { - return func.apply(context, arguments); - }; -} -``` - -Код станет таким: - -```js -//+ no-beautify -function f() { - alert(this.name); -} - -f = bind(f, {name: "Вася"} ); // (1) -f = bind(f, {name: "Петя"} ); // (2) - -f(); // Вася -``` - -Здесь видно, что первый вызов `bind`, в строке `(1)`, возвращает обёртку вокруг `f`, которая выглядит так (выделена): - -```js -function bind(func, context) { -*!* - return function() { - // здесь this не используется - return func.apply(context, arguments); - }; -*/!* -} -``` - -В этой обёртке нигде не используется `this`, контекст `context` берётся из замыкания. Посмотрите на код, там нигде нет `this`. - -Поэтому следующий `bind` в строке `(2)`, который выполняется уже над обёрткой и фиксирует в ней `this`, ни на что не влияет. Какая разница, что будет в качестве `this` в функции, которая этот `this` не использует? Контекст `context`, как видно в коде выше, она получает через замыкание из аргументов первого `bind`. - diff --git a/1-js/6-objects-more/7-bind/3-second-bind/task.md b/1-js/6-objects-more/7-bind/3-second-bind/task.md deleted file mode 100644 index 3b0f06b2..00000000 --- a/1-js/6-objects-more/7-bind/3-second-bind/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# Повторный bind - -[importance 5] - -Что выведет этот код? - -```js -//+ no-beautify -function f() { - alert(this.name); -} - -f = f.bind( {name: "Вася"} ).bind( {name: "Петя" } ); - -f(); -``` - diff --git a/1-js/6-objects-more/7-bind/4-function-property-after-bind/solution.md b/1-js/6-objects-more/7-bind/4-function-property-after-bind/solution.md deleted file mode 100644 index a882a35e..00000000 --- a/1-js/6-objects-more/7-bind/4-function-property-after-bind/solution.md +++ /dev/null @@ -1,4 +0,0 @@ -Ответ: `undefined`. - -Результатом работы `bind` является функция-обёртка над `sayHi`. Эта функция -- самостоятельный объект, у неё уже нет свойства `test`. - diff --git a/1-js/6-objects-more/7-bind/4-function-property-after-bind/task.md b/1-js/6-objects-more/7-bind/4-function-property-after-bind/task.md deleted file mode 100644 index b182b4e5..00000000 --- a/1-js/6-objects-more/7-bind/4-function-property-after-bind/task.md +++ /dev/null @@ -1,22 +0,0 @@ -# Свойство функции после bind - -[importance 5] - -В свойство функции записано значение. Изменится ли оно после применения `bind`? Обоснуйте ответ. - -```js -function sayHi() { - alert( this.name ); -} -sayHi.test = 5; -alert( sayHi.test ); // 5 - -*!* -var bound = sayHi.bind({ - name: "Вася" -}); - -alert( bound.test ); // что выведет? почему? -*/!* -``` - diff --git a/1-js/6-objects-more/7-bind/5-question-use-bind/solution.md b/1-js/6-objects-more/7-bind/5-question-use-bind/solution.md deleted file mode 100644 index 7eb5963e..00000000 --- a/1-js/6-objects-more/7-bind/5-question-use-bind/solution.md +++ /dev/null @@ -1,110 +0,0 @@ -# Решение с bind - -Ошибка происходит потому, что `ask` получает только функцию, без объекта-контекста. - -Используем `bind`, чтобы передать в `ask` функцию с уже привязанным контекстом: - -```js -//+ run -"use strict"; - -function ask(question, answer, ok, fail) { - var result = prompt(question, ''); - if (result.toLowerCase() == answer.toLowerCase()) ok(); - else fail(); -} - -var user = { - login: 'Василий', - password: '12345', - - loginOk: function() { - alert( this.login + ' вошёл в сайт' ); - }, - - loginFail: function() { - alert( this.login + ': ошибка входа' ); - }, - - checkPassword: function() { -*!* - ask("Ваш пароль?", this.password, this.loginOk.bind(this), this.loginFail.bind(this)); -*/!* - } -}; - -var vasya = user; -user = null; -vasya.checkPassword(); -``` - -# Решение через замыкание - -Альтернативное решение -- сделать функции-обёртки над `user.loginOk/loginFail`: - -```js -//+ no-beautify -var user = { - ... - checkPassword: function() { -*!* - ask("Ваш пароль?", this.password, - function() { user.loginOk(); }, function() { user.loginFail(); }); -*/!* - } -} -``` - -...Но такой код использует переменную `user`, так что если объект переместить из неё, к примеру, так, то работать он не будет: - -```js -var vasya = user; // переместим user в vasya -user = null; -vasya.checkPassword(); // упс будет ошибка, ведь в коде объекта остался user -``` - -Для того, чтобы избежать проблем, можно использовать `this`. Внутри `checkPassword` он всегда будет равен текущему объекту, так что скопируем его в переменную, которую назовём `self`: - -```js -//+ run -"use strict"; - -function ask(question, answer, ok, fail) { - var result = prompt(question, ''); - if (result.toLowerCase() == answer.toLowerCase()) ok(); - else fail(); -} - -var user = { - login: 'Василий', - password: '12345', - - loginOk: function() { - alert( this.login + ' вошёл в сайт' ); - }, - - loginFail: function() { - alert( this.login + ': ошибка входа' ); - }, - - checkPassword: function() { -*!* - var self = this; - ask("Ваш пароль?", this.password, - function() { - self.loginOk(); - }, - function() { - self.loginFail(); - } - ); -*/!* - } -}; - -var vasya = user; -user = null; -vasya.checkPassword(); -``` - -Теперь всё работает. Анонимные функции достают правильный контекст из замыкания, где он сохранён в переменной `self`. \ No newline at end of file diff --git a/1-js/6-objects-more/7-bind/5-question-use-bind/task.md b/1-js/6-objects-more/7-bind/5-question-use-bind/task.md deleted file mode 100644 index a36bb858..00000000 --- a/1-js/6-objects-more/7-bind/5-question-use-bind/task.md +++ /dev/null @@ -1,50 +0,0 @@ -# Использование функции вопросов - -[importance 5] - -Вызов `user.checkPassword()` в коде ниже должен, при помощи `ask`, спрашивать пароль и вызывать `loginOk/loginFail` в зависимости от правильности ответа. - -Однако, его вызов приводит к ошибке. Почему? - -Исправьте выделенную строку, чтобы всё работало (других строк изменять не надо). - -```js -//+ run -"use strict"; - -function ask(question, answer, ok, fail) { - var result = prompt(question, ''); - if (result.toLowerCase() == answer.toLowerCase()) ok(); - else fail(); -} - -var user = { - login: 'Василий', - password: '12345', - - loginOk: function() { - alert( this.login + ' вошёл в сайт' ); - }, - - loginFail: function() { - alert( this.login + ': ошибка входа' ); - }, - - checkPassword: function() { -*!* - ask("Ваш пароль?", this.password, this.loginOk, this.loginFail); -*/!* - } -}; - -user.checkPassword(); -``` - -P.S. Ваше решение должно также срабатывать, если переменная `user` будет перезаписана, например вместо `user.checkPassword()` в конце будут строки: - -```js -var vasya = user; -user = null; -vasya.checkPassword(); -``` - diff --git a/1-js/6-objects-more/7-bind/6-ask-currying/solution.md b/1-js/6-objects-more/7-bind/6-ask-currying/solution.md deleted file mode 100644 index 86877551..00000000 --- a/1-js/6-objects-more/7-bind/6-ask-currying/solution.md +++ /dev/null @@ -1,73 +0,0 @@ -# Решение с bind - -Первое решение -- передать в `ask` функции с привязанным контекстом и аргументами. - -```js -//+ run -"use strict"; - -function ask(question, answer, ok, fail) { - var result = prompt(question, ''); - if (result.toLowerCase() == answer.toLowerCase()) ok(); - else fail(); -} - -var user = { - login: 'Василий', - password: '12345', - - loginDone: function(result) { - alert( this.login + (result ? ' вошёл в сайт' : ' ошибка входа') ); - }, - - checkPassword: function() { -*!* - ask("Ваш пароль?", this.password, this.loginDone.bind(this, true), this.loginDone.bind(this, false)); -*/!* - } -}; - -user.checkPassword(); -``` - -# Решение с локальной переменной - -Второе решение -- это скопировать `this` в локальную переменную (чтобы внешняя перезапись не повлияла): - -```js -//+ run -"use strict"; - -function ask(question, answer, ok, fail) { - var result = prompt(question, ''); - if (result.toLowerCase() == answer.toLowerCase()) ok(); - else fail(); -} - -var user = { - login: 'Василий', - password: '12345', - - loginDone: function(result) { - alert( this.login + (result ? ' вошёл в сайт' : ' ошибка входа') ); - }, - - checkPassword: function() { - var self = this; -*!* - ask("Ваш пароль?", this.password, - function() { - self.loginDone(true); - }, - function() { - self.loginDone(false); - } - ); -*/!* - } -}; - -user.checkPassword(); -``` - -Оба решения хороши, вариант с `bind` короче. \ No newline at end of file diff --git a/1-js/6-objects-more/7-bind/6-ask-currying/task.md b/1-js/6-objects-more/7-bind/6-ask-currying/task.md deleted file mode 100644 index 7b48e190..00000000 --- a/1-js/6-objects-more/7-bind/6-ask-currying/task.md +++ /dev/null @@ -1,57 +0,0 @@ -# Использование функции вопросов с каррингом - -[importance 5] - -Эта задача -- усложнённый вариант задачи [](/task/question-use-bind). В ней объект `user` изменён. - -Теперь заменим две функции `user.loginOk()` и `user.loginFail()` на единый метод: `user.loginDone(true/false)`, который нужно вызвать с `true` при верном ответе и `fail` -- при неверном. - -Код ниже делает это, соответствующий фрагмент выделен. - -**Сейчас он обладает важным недостатком: при записи в `user` другого значения объект перестанет корректно работать, вы увидите это, запустив пример ниже (будет ошибка).** - -Как бы вы написали правильно? - -**Исправьте выделенный фрагмент, чтобы код заработал.** - -```js -//+ run -"use strict"; - -function ask(question, answer, ok, fail) { - var result = prompt(question, ''); - if (result.toLowerCase() == answer.toLowerCase()) ok(); - else fail(); -} - -var user = { - login: 'Василий', - password: '12345', - - // метод для вызова из ask - loginDone: function(result) { - alert( this.login + (result ? ' вошёл в сайт' : ' ошибка входа') ); - }, - - checkPassword: function() { -*!* - ask("Ваш пароль?", this.password, - function() { - user.loginDone(true); - }, - function() { - user.loginDone(false); - } - ); -*/!* - } -}; - -var vasya = user; -user = null; -vasya.checkPassword(); -``` - -Изменения должны касаться только выделенного фрагмента. - -Если возможно, предложите два решения, одно -- с использованием `bind`, другое -- без него. Какое решение лучше? diff --git a/1-js/6-objects-more/7-bind/article.md b/1-js/6-objects-more/7-bind/article.md deleted file mode 100644 index a18a910e..00000000 --- a/1-js/6-objects-more/7-bind/article.md +++ /dev/null @@ -1,434 +0,0 @@ -# Привязка контекста и карринг: "bind" - -Функции в JavaScript никак не привязаны к своему контексту `this`, с одной стороны, здорово -- это позволяет быть максимально гибкими, одалживать методы и так далее. - -Но с другой стороны -- в некоторых случаях контекст может быть потерян. То есть мы вроде как вызываем метод объекта, а на самом деле он получает `this = undefined`. - -Такая ситуация является типичной для начинающих разработчиков, но бывает и у "зубров" тоже. Конечно, "зубры" при этом знают, что с ней делать. - -[cut] - -## Пример потери контекста - -В браузере есть встроенная функция `setTimeout(func, ms)`, которая вызывает выполение функции `func` через `ms` миллисекунд (=1/1000 секунды). - -Мы подробно остановимся на ней и её тонкостях позже, в главе [](/settimeout-setinterval), а пока просто посмотрим пример. - -Этот код выведет "Привет" через 1000мс, то есть 1 секунду: - -```js -//+ run -setTimeout(function() { - alert( "Привет" ); -}, 1000); -``` - -Попробуем сделать то же самое с методом объекта, следующий код должен выводить имя пользователя через 1 секунду: - -```js -//+ run -var user = { - firstName: "Вася", - sayHi: function() { - alert( this.firstName ); - } -}; - -*!* -setTimeout(user.sayHi, 1000); // undefined (не Вася!) -*/!* -``` - -При запуске кода выше через секунду выводится вовсе не `"Вася"`, а `undefined`! - -Это произошло потому, что в примере выше `setTimeout` получил функцию `user.sayHi`, но не её контекст. То есть, последняя строчка аналогична двум таким: - -```js -var f = user.sayHi; -setTimeout(f, 1000); // контекст user потеряли -``` - - -Ситуация довольно типична -- мы хотим передать метод объекта куда-то в другое место кода, откуда он потом может быть вызван. Как бы прикрепить к нему контекст, желательно, с минимумом плясок с бубном и при этом надёжно? - -Есть несколько способов решения, среди которых мы, в зависимости от ситуации, можем выбирать. - -## Решение 1: сделать обёртку - -Самый простой вариант решения -- это обернуть вызов в анонимную функцию: - -```js -//+ run -var user = { - firstName: "Вася", - sayHi: function() { - alert( this.firstName ); - } -}; - -*!* -setTimeout(function() { - user.sayHi(); // Вася -}, 1000); -*/!* -``` - -Теперь код работает, так как `user` достаётся из замыкания. - -Это решение также позволяет передать дополнительные аргументы: - - -```js -//+ run -var user = { - firstName: "Вася", - sayHi: function(who) { - alert( this.firstName + ": Привет, " + who ); - } -}; - -*!* -setTimeout(function() { - user.sayHi("Петя"); // Вася: Привет, Петя -}, 1000); -*/!* -``` - - -Но тут же появляется и уязвимое место в структуре кода! - -А что, если до срабатывания `setTimeout` (ведь есть целая секунда) в переменную `user` будет записано другое значение? К примеру, в другом месте кода будет присвоено `user=(другой пользователь)`... В этом случае вызов неожиданно будет совсем не тот! - -Хорошо бы гарантировать правильность контекста. - -## Решение 2: bind для привязки контекста - -Напишем вспомогательную функцию `bind(func, context)`, которая будет жёстко фиксировать контекст для `func`: - -```js -function bind(func, context) { - return function() { // (*) - return func.apply(context, arguments); - }; -} -``` - -Посмотрим, что она делает, как работает, на таком примере: - -```js -//+ run -function f() { - alert( this ); -} - -var g = bind(f, "Context"); -g(); // Context -``` - -То есть, `bind(f, "Context")` привязывает `"Context"` в качестве `this` для `f`. - -Посмотрим, за счёт чего это происходит. - -Результатом `bind(f, "Context")`, как видно из кода, будет анонимная функция `(*)`. - -Вот она отдельно: - -```js -function() { // (*) - return func.apply(context, arguments); -}; -``` - -Если подставить наши конкретные аргументы, то есть `f` и `"Context"`, то получится так: - -```js -function() { // (*) - return f.apply("Context", arguments); -}; -``` - -Эта функция запишется в переменную `g`. - -Далее, если вызвать `g`, то вызов будет передан в `f`, причём `f.apply("Context", arguments)` передаст в качестве контекста `"Context"`, который и будет выведен. - -Если вызвать `g` с аргументами, то также будет работать: - -```js -//+ run -function f(a, b) { - alert( this ); - alert( a + b ); -} - -var g = bind(f, "Context"); -g(1, 2); // Context, затем 3 -``` - -Аргументы, которые получила `g(...)`, передаются в `f` также благодаря методу `.apply`. - -**Иными словами, в результате вызова `bind(func, context)` мы получаем "функцию-обёртку", которая прозрачно передаёт вызов в `func`, с теми же аргументами, но фиксированным контекстом `context`.** - -Вернёмся к `user.sayHi`. Вариант с `bind`: - -```js -//+ run -function bind(func, context) { - return function() { - return func.apply(context, arguments); - }; -} - -var user = { - firstName: "Вася", - sayHi: function() { - alert( this.firstName ); - } -}; - -*!* -setTimeout(bind(user.sayHi, user), 1000); -*/!* -``` - -Теперь всё в порядке! - -Вызов `bind(user.sayHi, user)` возвращает такую функцию-обёртку, которая привязывает `user.sayHi` к контексту `user`. Она будет вызвана через 1000мс. - -Полученную обёртку можно вызвать и с аргументами -- они пойдут в `user.sayHi` без изменений, фиксирован лишь контекст. - -```js -//+ run -var user = { - firstName: "Вася", -*!* - sayHi: function(who) { // здесь у sayHi есть один аргумент -*/!* - alert( this.firstName + ": Привет, " + who ); - } -}; - -var sayHi = bind(user.sayHi, user); - -*!* -// контекст Вася, а аргумент передаётся "как есть" -sayHi("Петя"); // Вася: Привет, Петя -sayHi("Маша"); // Вася: Привет, Маша -*/!* -``` - -В примере выше продемонстрирована другая частая цель использования `bind` -- "привязать" функцию к контексту, чтобы в дальнейшем "не таскать за собой" объект, а просто вызывать `sayHi`. - -Результат `bind` можно передавать в любое место кода, вызывать как обычную функцию, он "помнит" свой контекст. - -## Решение 3: встроенный метод bind [#bind] - -В современном JavaScript (или при подключении библиотеки [es5-shim](https://github.com/kriskowal/es5-shim) для IE8-) у функций уже есть встроенный метод [bind](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind), который мы можем использовать. - -Он работает примерно так же, как `bind`, который описан выше. - -Изменения очень небольшие: - -```js -//+ run -function f(a, b) { - alert( this ); - alert( a + b ); -} - -*!* -// вместо -// var g = bind(f, "Context"); -var g = f.bind("Context"); -*/!* -g(1, 2); // Context, затем 3 -``` - -Синтаксис встроенного `bind`: - -```js -var wrapper = func.bind(context[, arg1, arg2...]) -``` - -
        -
        `func`
        -
        Произвольная функция
        -
        `context`
        -
        Контекст, который привязывается к `func`
        -
        `arg1`, `arg2`, ...
        -
        Если указаны аргументы `arg1, arg2...` -- они будут прибавлены к каждому вызову новой функции, причем встанут *перед* теми, которые указаны при вызове.
        -
        - -Результат вызова `func.bind(context)` аналогичен вызову `bind(func, context)`, описанному выше. То есть, `wrapper` -- это обёртка, фиксирующая контекст и передающая вызовы в `func`. Также можно указать аргументы, тогда и они будут фиксированы, но об этом чуть позже. - -Пример со встроенным методом `bind`: - -```js -//+ run -var user = { - firstName: "Вася", - sayHi: function() { - alert( this.firstName ); - } -}; - -*!* -// setTimeout( bind(user.sayHi, user), 1000 ); -setTimeout(user.sayHi.bind(user), 1000); // аналог через встроенный метод -*/!* -``` - -Получили простой и надёжный способ привязать контекст, причём даже встроенный в JavaScript. - -Далее мы будем использовать именно встроенный метод `bind`. - -[warn header="bind не похож на call/apply"] -Методы `bind` и `call/apply` близки по синтаксису, но есть важнейшее отличие. - -Методы `call/apply` вызывают функцию с заданным контекстом и аргументами. - -А `bind` не вызывает функцию. Он только возвращает "обёртку", которую мы можем вызвать позже, и которая передаст вызов в исходную функцию, с привязанным контекстом. -[/warn] - -[smart header="Привязать всё: `bindAll`"] -Если у объекта много методов и мы планируем их активно передавать, то можно привязать контекст для них всех в цикле: - -```js -for (var prop in user) { - if (typeof user[prop] == 'function') { - user[prop] = user[prop].bind(user); - } -} -``` - -В некоторых JS-фреймворках есть даже встроенные функции для этого, например [_.bindAll(obj)](http://lodash.com/docs#bindAll). -[/smart] - - -## Карринг - -До этого мы говорили о привязке контекста. Теперь пойдём на шаг дальше. Привязывать можно не только контекст, но и аргументы. Используется это реже, но бывает полезно. - -[Карринг](http://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D1%80%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) (currying) или *каррирование* -- термин [функционального программирования](http://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5), который означает создание новой функции путём фиксирования аргументов существующей. - -Как было сказано выше, метод `func.bind(context, ...)` может создавать обёртку, которая фиксирует не только контекст, но и ряд аргументов функции. - -Например, есть функция умножения двух чисел `mul(a, b)`: - -```js -function mul(a, b) { - return a * b; -}; -``` - -При помощи `bind` создадим функцию `double`, удваивающую значения. Это будет вариант функции `mul` с фиксированным первым аргументом: - -```js -//+ run -*!* -// double умножает только на два -var double = mul.bind(null, 2); // контекст фиксируем null, он не используется -*/!* - -alert( double(3) ); // = mul(2, 3) = 6 -alert( double(4) ); // = mul(2, 4) = 8 -alert( double(5) ); // = mul(2, 5) = 10 -``` - -При вызове `double` будет передавать свои аргументы исходной функции `mul` после тех, которые указаны в `bind`, то есть в данном случае после зафиксированного первого аргумента `2`. - -**Говорят, что `double` является "частичной функцией" (partial function) от `mul`.** - -Другая частичная функция `triple` утраивает значения: - -```js -//+ run -*!* -var triple = mul.bind(null, 3); // контекст фиксируем null, он не используется -*/!* - -alert( triple(3) ); // = mul(3, 3) = 9 -alert( triple(4) ); // = mul(3, 4) = 12 -alert( triple(5) ); // = mul(3, 5) = 15 -``` - -При помощи `bind` мы можем получить из функции её "частный вариант" как самостоятельную функцию и дальше передать в `setTimeout` или сделать с ней что-то ещё. - -Наш выигрыш состоит в том, что эта самостоятельная функция, во-первых, имеет понятное имя (`double`, `triple`), а во-вторых, повторные вызовы позволяют не указывать каждый раз первый аргумент, он уже фиксирован благодаря `bind`. - -## Функция ask для задач - -В задачах этого раздела предполагается, что объявлена следующая "функция вопросов" `ask`: - -```js -function ask(question, answer, ok, fail) { - var result = prompt(question, ''); - if (result.toLowerCase() == answer.toLowerCase()) ok(); - else fail(); -} -``` - -Её назначение -- задать вопрос `question` и, если ответ совпадёт с `answer`, то запустить функцию `ok()`, а иначе -- функцию `fail()`. - -Несмотря на внешнюю простоту, функции такого вида активно используются в реальных проектах. Конечно, они будут сложнее, вместо `alert/prompt` -- вывод красивого JavaScript-диалога с рамочками, кнопочками и так далее, но это нам сейчас не нужно. - -Пример использования: - -```js -//+ run -*!* -ask("Выпустить птичку?", "да", fly, die); -*/!* - -function fly() { - alert( 'улетела :)' ); -} - -function die() { - alert( 'птичку жалко :(' ); -} -``` - -## Итого - -
          -
        • Функция сама по себе не запоминает контекст выполнения.
        • -
        • Чтобы гарантировать правильный контекст для вызова `obj.func()`, нужно использовать функцию-обёртку, задать её через анонимную функцию: -```js -setTimeout(function() { - obj.func(); -}) -``` -
        • -
        • ...Либо использовать `bind`: - -```js -setTimeout(obj.func.bind(obj)); -``` -
        • -
        • Вызов `bind` часто используют для привязки функции к контексту, чтобы затем присвоить её в обычную переменную и вызывать уже без явного указания объекта.
        • -
        • Вызов `bind` также позволяет фиксировать первые аргументы функции ("каррировать" её), и таким образом из общей функции получить её "частные" варианты -- чтобы использовать их многократно без повтора одних и тех же аргументов каждый раз.
        • -
        - -[head] - -[/head] diff --git a/1-js/6-objects-more/8-decorators/1-logging-decorator/_js.view/solution.js b/1-js/6-objects-more/8-decorators/1-logging-decorator/_js.view/solution.js deleted file mode 100644 index 1aa8f279..00000000 --- a/1-js/6-objects-more/8-decorators/1-logging-decorator/_js.view/solution.js +++ /dev/null @@ -1,9 +0,0 @@ -function makeLogging(f, log) { - - function wrapper(a) { - log.push(a); - return f.call(this, a); - } - - return wrapper; -} \ No newline at end of file diff --git a/1-js/6-objects-more/8-decorators/1-logging-decorator/_js.view/test.js b/1-js/6-objects-more/8-decorators/1-logging-decorator/_js.view/test.js deleted file mode 100644 index 2257ee89..00000000 --- a/1-js/6-objects-more/8-decorators/1-logging-decorator/_js.view/test.js +++ /dev/null @@ -1,50 +0,0 @@ -describe("makeLogging", function() { - it("записывает вызовы в массив log", function() { - var work = sinon.spy(); - - var log = []; - work = makeLogging(work, log); - assert.deepEqual(log, []); - - work(1); - assert.deepEqual(log, [1]); - - work(2); - assert.deepEqual(log, [1, 2]); - }); - - it("передаёт вызов функции, возвращает её результат", function() { - var log = []; - - function work(x) { - return x * 2; - } - - work = sinon.spy(work); - var spy = work; - work = makeLogging(work, log); - - assert.equal(work(1), 2); - assert(spy.calledWith(1)); - }); - - - it("сохраняет контекст вызова для методов объекта", function() { - var log = []; - - var calculator = { - double: function(x) { - return x * 2; - } - } - - calculator.double = sinon.spy(calculator.double); - var spy = calculator.double; - calculator.double = makeLogging(calculator.double, log); - - assert.equal(calculator.double(1), 2); - assert(spy.calledWith(1)); - assert(spy.calledOn(calculator)); - }); - -}); \ No newline at end of file diff --git a/1-js/6-objects-more/8-decorators/1-logging-decorator/solution.md b/1-js/6-objects-more/8-decorators/1-logging-decorator/solution.md deleted file mode 100644 index 103ff4e5..00000000 --- a/1-js/6-objects-more/8-decorators/1-logging-decorator/solution.md +++ /dev/null @@ -1,44 +0,0 @@ -Возвратим декоратор `wrapper` который будет записывать аргумент в `log` и передавать вызов в `f`: - -```js -//+ run -function work(a) { - /*...*/ // work - произвольная функция, один аргумент -} - -function makeLogging(f, log) { - -*!* - function wrapper(a) { - log.push(a); - return f.call(this, a); - } -*/!* - - return wrapper; -} - -var log = []; -work = makeLogging(work, log); - -work(1); // 1 -work(5); // 5 - -for (var i = 0; i < log.length; i++) { - alert( 'Лог:' + log[i] ); // "Лог:1", затем "Лог:5" -} -``` - -**Обратите внимание, вызов функции осуществляется как `f.call(this, a)`, а не просто `f(a)`.** - -Передача контекста необходима, чтобы декоратор корректно работал с методами объекта. Например: - -```js -user.method = makeLogging(user.method, log); -``` - -Теперь при вызове `user.method(...)` в декоратор будет передаваться контекст `this`, который надо передать исходной функции через `call/apply`. - - - - diff --git a/1-js/6-objects-more/8-decorators/1-logging-decorator/task.md b/1-js/6-objects-more/8-decorators/1-logging-decorator/task.md deleted file mode 100644 index e1efa4f8..00000000 --- a/1-js/6-objects-more/8-decorators/1-logging-decorator/task.md +++ /dev/null @@ -1,32 +0,0 @@ -# Логирующий декоратор (1 аргумент) - -[importance 5] - -Создайте декоратор `makeLogging(f, log)`, который берет функцию `f` и массив `log`. - -Он должен возвращать обёртку вокруг `f`, которая при каждом вызове записывает ("логирует") аргументы в `log`, а затем передает вызов в `f`. - -**В этой задаче можно считать, что у функции `f` ровно один аргумент.** - -Работать должно так: - -```js -function work(a) { - /* ... */ // work - произвольная функция, один аргумент -} - -function makeLogging(f, log) { /* ваш код */ } - -var log = []; -work = makeLogging(work, log); - -work(1); // 1, добавлено в log -work(5); // 5, добавлено в log - -for (var i = 0; i < log.length; i++) { -*!* - alert( 'Лог:' + log[i] ); // "Лог:1", затем "Лог:5" -*/!* -} -``` - diff --git a/1-js/6-objects-more/8-decorators/2-logging-decorator-arguments/_js.view/solution.js b/1-js/6-objects-more/8-decorators/2-logging-decorator-arguments/_js.view/solution.js deleted file mode 100644 index f7052f65..00000000 --- a/1-js/6-objects-more/8-decorators/2-logging-decorator-arguments/_js.view/solution.js +++ /dev/null @@ -1,9 +0,0 @@ -function makeLogging(f, log) { - - function wrapper() { - log.push([].slice.call(arguments)); - return f.apply(this, arguments); - } - - return wrapper; -} \ No newline at end of file diff --git a/1-js/6-objects-more/8-decorators/2-logging-decorator-arguments/_js.view/test.js b/1-js/6-objects-more/8-decorators/2-logging-decorator-arguments/_js.view/test.js deleted file mode 100644 index eb29c837..00000000 --- a/1-js/6-objects-more/8-decorators/2-logging-decorator-arguments/_js.view/test.js +++ /dev/null @@ -1,55 +0,0 @@ -describe("makeLogging", function() { - it("записывает вызовы в массив log", function() { - var work = sinon.spy(); - - var log = []; - work = makeLogging(work, log); - assert.deepEqual(log, []); - - work(1, 2); - assert.deepEqual(log, [ - [1, 2] - ]); - - work(3, 4); - assert.deepEqual(log, [ - [1, 2], - [3, 4] - ]); - }); - - it("передаёт вызов функции, возвращает её результат", function() { - var log = []; - - function sum(a, b) { - return a + b; - } - - sum = sinon.spy(sum); - var spy = sum; - sum = makeLogging(sum, log); - - assert.equal(sum(1, 2), 3); - assert(spy.calledWith(1, 2)); - }); - - - it("сохраняет контекст вызова для методов объекта", function() { - var log = []; - - var calculator = { - sum: function(a, b) { - return a + b; - } - } - - calculator.sum = sinon.spy(calculator.sum); - var spy = calculator.sum; - calculator.sum = makeLogging(calculator.sum, log); - - assert.equal(calculator.sum(1, 2), 3); - assert(spy.calledWith(1, 2)); - assert(spy.calledOn(calculator)); - }); - -}); \ No newline at end of file diff --git a/1-js/6-objects-more/8-decorators/2-logging-decorator-arguments/solution.md b/1-js/6-objects-more/8-decorators/2-logging-decorator-arguments/solution.md deleted file mode 100644 index dfc2b902..00000000 --- a/1-js/6-objects-more/8-decorators/2-logging-decorator-arguments/solution.md +++ /dev/null @@ -1,34 +0,0 @@ -Решение аналогично задаче [](/task/logging-decorator), разница в том, что в лог вместо одного аргумента идет весь объект `arguments`. - -Для передачи вызова с произвольным количеством аргументов используем `f.apply(this, arguments)`. - -```js -//+ run -function work(a, b) { - alert( a + b ); // work - произвольная функция -} - -function makeLogging(f, log) { - -*!* - function wrapper() { - log.push([].slice.call(arguments)); - return f.apply(this, arguments); - } -*/!* - - return wrapper; -} - -var log = []; -work = makeLogging(work, log); - -work(1, 2); // 3 -work(4, 5); // 9 - -for (var i = 0; i < log.length; i++) { - var args = log[i]; // массив из аргументов i-го вызова - alert( 'Лог:' + args.join() ); // "Лог:1,2", "Лог:4,5" -} -``` - diff --git a/1-js/6-objects-more/8-decorators/2-logging-decorator-arguments/task.md b/1-js/6-objects-more/8-decorators/2-logging-decorator-arguments/task.md deleted file mode 100644 index e5ab5b6b..00000000 --- a/1-js/6-objects-more/8-decorators/2-logging-decorator-arguments/task.md +++ /dev/null @@ -1,29 +0,0 @@ -# Логирующий декоратор (много аргументов) - -[importance 3] - -Создайте декоратор `makeLogging(func, log)`, для функции `func` возвращающий обёртку, которая при каждом вызове добавляет её аргументы в массив `log`. - -Условие аналогично задаче [](/task/logging-decorator), но допускается `func` с любым набором аргументов. - -Работать должно так: - -```js -function work(a, b) { - alert( a + b ); // work - произвольная функция -} - -function makeLogging(f, log) { /* ваш код */ } - -var log = []; -work = makeLogging(work, log); - -work(1, 2); // 3 -work(4, 5); // 9 - -for (var i = 0; i < log.length; i++) { - var args = log[i]; // массив из аргументов i-го вызова - alert( 'Лог:' + args.join() ); // "Лог:1,2", "Лог:4,5" -} -``` - diff --git a/1-js/6-objects-more/8-decorators/3-caching-decorator/_js.view/solution.js b/1-js/6-objects-more/8-decorators/3-caching-decorator/_js.view/solution.js deleted file mode 100644 index dc865601..00000000 --- a/1-js/6-objects-more/8-decorators/3-caching-decorator/_js.view/solution.js +++ /dev/null @@ -1,11 +0,0 @@ -function makeCaching(f) { - var cache = {}; - - return function(x) { - if (!(x in cache)) { - cache[x] = f.call(this, x); - } - return cache[x]; - }; - -} \ No newline at end of file diff --git a/1-js/6-objects-more/8-decorators/3-caching-decorator/_js.view/test.js b/1-js/6-objects-more/8-decorators/3-caching-decorator/_js.view/test.js deleted file mode 100644 index aed6f0fe..00000000 --- a/1-js/6-objects-more/8-decorators/3-caching-decorator/_js.view/test.js +++ /dev/null @@ -1,31 +0,0 @@ -describe("makeCaching", function() { - - it("запоминает предыдущее значение функции с таким аргументом", function() { - function f(x) { - return Math.random() * x; - } - - f = makeCaching(f); - - var a = f(1); - var b = f(1); - assert.equal(a, b); - - var anotherValue = f(2); - // почти наверняка другое значение - assert.notEqual(a, anotherValue); - }); - - it("сохраняет контекст вызова", function() { - var obj = { - spy: sinon.spy() - }; - - var spy = obj.spy; - obj.spy = makeCaching(obj.spy); - obj.spy(123); - assert(spy.calledWith(123)); - assert(spy.calledOn(obj)); - }); - -}); \ No newline at end of file diff --git a/1-js/6-objects-more/8-decorators/3-caching-decorator/solution.md b/1-js/6-objects-more/8-decorators/3-caching-decorator/solution.md deleted file mode 100644 index 9b413f92..00000000 --- a/1-js/6-objects-more/8-decorators/3-caching-decorator/solution.md +++ /dev/null @@ -1,34 +0,0 @@ -Запоминать результаты вызова функции будем в замыкании, в объекте `cache: { ключ:значение }`. - -```js -//+ run no-beautify -function f(x) { - return Math.random()*x; -} - -*!* -function makeCaching(f) { - var cache = {}; - - return function(x) { - if (!(x in cache)) { - cache[x] = f.call(this, x); - } - return cache[x]; - }; - -} -*/!* - -f = makeCaching(f); - -var a = f(1); -var b = f(1); -alert( a == b ); // true (значение закешировано) - -b = f(2); -alert( a == b ); // false, другой аргумент => другое значение -``` - -Обратите внимание: проверка на наличие уже подсчитанного значения выглядит так: `if (x in cache)`. Менее универсально можно проверить так: `if (cache[x])`, это если мы точно знаем, что `cache[x]` никогда не будет `false`, `0` и т.п. - diff --git a/1-js/6-objects-more/8-decorators/3-caching-decorator/task.md b/1-js/6-objects-more/8-decorators/3-caching-decorator/task.md deleted file mode 100644 index f0b3a78f..00000000 --- a/1-js/6-objects-more/8-decorators/3-caching-decorator/task.md +++ /dev/null @@ -1,34 +0,0 @@ -# Кеширующий декоратор - -[importance 5] - -Создайте декоратор `makeCaching(f)`, который берет функцию `f` и возвращает обертку, которая кеширует её результаты. - -**В этой задаче функция `f` имеет только один аргумент, и он является числом.** - -
          -
        1. При первом вызове обертки с определенным аргументом -- она вызывает `f` и запоминает значение.
        2. -
        3. При втором и последующих вызовах с тем же аргументом возвращается запомненное значение.
        4. -
        - -Должно работать так: - -```js -function f(x) { - return Math.random() * x; // random для удобства тестирования -} - -function makeCaching(f) { /* ваш код */ } - -f = makeCaching(f); - -var a, b; - -a = f(1); -b = f(1); -alert( a == b ); // true (значение закешировано) - -b = f(2); -alert( a == b ); // false, другой аргумент => другое значение -``` - diff --git a/1-js/6-objects-more/8-decorators/article.md b/1-js/6-objects-more/8-decorators/article.md deleted file mode 100644 index f089df90..00000000 --- a/1-js/6-objects-more/8-decorators/article.md +++ /dev/null @@ -1,232 +0,0 @@ -# Функции-обёртки, декораторы - -JavaScript предоставляет удивительно гибкие возможности по работе с функциями: их можно передавать, в них можно записывать данные как в объекты, у них есть свои встроенные методы... - -Конечно, этим нужно уметь пользоваться. В этой главе, чтобы более глубоко понимать работу с функциями, мы рассмотрим создание функций-обёрток или, иначе говоря, "декораторов". - -[cut] - -[Декоратор](http://en.wikipedia.org/wiki/Decorator_pattern) -- приём программирования, который позволяет взять существующую функцию и изменить/расширить ее поведение. - -*Декоратор* получает функцию и возвращает обертку, которая делает что-то своё "вокруг" вызова основной функции. - -## bind -- привязка контекста - -Один простой декоратор вы уже видели ранее -- это функция [bind](/bind): - -```js -function bind(func, context) { - return function() { - return func.apply(context, arguments); - }; -} -``` - -Вызов `bind(func, context)` возвращает обёртку, которая ставит `this` и передаёт основную работу функции `func`. - -## Декоратор-таймер - -Создадим более сложный декоратор, замеряющий время выполнения функции. - -Он будет называться `timingDecorator` и получать функцию вместе с "названием таймера", а возвращать -- функцию-обёртку, которая измеряет время и прибавляет его в специальный объект `timer` по свойству-названию. - -Использование: -```js -function f(x) {} // любая функция - -var timers = {}; // объект для таймеров - -// отдекорировали -f = timingDecorator(f, "myFunc"); - -// запускаем -f(1); -f(2); -f(3); // функция работает как раньше, но время подсчитывается - -alert( timers.myFunc ); // общее время выполнения всех вызовов f -``` - -При помощи декоратора `timingDecorator` мы сможем взять произвольную функцию и одним движением руки прикрутить к ней измеритель времени. - -Его реализация: - -```js -//+ run -var timers = {}; - -// прибавит время выполнения f к таймеру timers[timer] -function timingDecorator(f, timer) { - return function() { - var start = performance.now(); - - var result = f.apply(this, arguments); // (*) - - if (!timers[timer]) timers[timer] = 0; - timers[timer] += performance.now() - start; - - return result; - } -} - -// функция может быть произвольной, например такой: -function fibonacci(n) { - return (n > 2) ? fibonacci(n - 1) + fibonacci(n - 2) : 1; -} - -*!* -// использование: завернём fibonacci в декоратор -fibonacci = timingDecorator(fibonacci, "fibo"); -*/!* - -// неоднократные вызовы... -alert( fibonacci(10) ); // 55 -alert( fibonacci(20) ); // 6765 -// ... - -*!* -// в любой момент можно получить общее количество времени на вызовы -alert( timers.fibo + 'мс' ); -*/!* -``` - -Обратим внимание на строку `(*)` внутри декоратора, которая и осуществляет передачу вызова: - -```js -var result = f.apply(this, arguments); // (*) -``` - -Этот приём называется "форвардинг вызова" (от англ. forwarding): текущий контекст и аргументы через `apply` передаются в функцию `f`, так что изнутри `f` всё выглядит так, как была вызвана она напрямую, а не декоратор. - -## Декоратор для проверки типа - -В JavaScript, как правило, пренебрегают проверками типа. В функцию, которая должна получать число, может быть передана строка, булево значение или даже объект. - -Например: - -```js -//+ no-beautify -function sum(a, b) { - return a + b; -} - -// передадим в функцию для сложения чисел нечисловые значения -alert( sum(true, { name: "Вася", age: 35 }) ); // true[Object object] -``` - -Функция "как-то" отработала, но в реальной жизни передача в `sum` подобных значений, скорее всего, будет следствием программной ошибки. Всё-таки `sum` предназначена для суммирования чисел, а не объектов. - -Многие языки программирования позволяют прямо в объявлении функции указать, какие типы данных имеют параметры. И это удобно, поскольку повышает надёжность кода. - -В JavaScript же проверку типов приходится делать дополнительным кодом в начале функции, который во-первых обычно лень писать, а во-вторых он увеличивает общий объем текста, тем самым ухудшая читаемость. - -**Декораторы способны упростить рутинные, повторяющиеся задачи, вынести их из кода функции.** - -Например, создадим декоратор, который принимает функцию и массив, который описывает для какого аргумента какую проверку типа применять: - -```js -//+ run -// вспомогательная функция для проверки на число -function checkNumber(value) { - return typeof value == 'number'; -} - -// декоратор, проверяющий типы для f -// второй аргумент checks - массив с функциями для проверки -function typeCheck(f, checks) { - return function() { - for (var i = 0; i < arguments.length; i++) { - if (!checks[i](arguments[i])) { - alert( "Некорректный тип аргумента номер " + i ); - return; - } - } - return f.apply(this, arguments); - } -} - -function sum(a, b) { - return a + b; -} - -*!* -// обернём декоратор для проверки -sum = typeCheck(sum, [checkNumber, checkNumber]); // оба аргумента - числа -*/!* - -// пользуемся функцией как обычно -alert( sum(1, 2) ); // 3, все хорошо - -*!* -// а вот так - будет ошибка -sum(true, null); // некорректный аргумент номер 0 -sum(1, ["array", "in", "sum?!?"]); // некорректный аргумент номер 1 -*/!* -``` - -Конечно, этот декоратор можно ещё расширять, улучшать, дописывать проверки, но... Вы уже поняли принцип, не правда ли? - -**Один раз пишем декоратор и дальше просто применяем этот функционал везде, где нужно.** - -## Декоратор проверки доступа - -И наконец посмотрим ещё один, последний пример. - -Предположим, у нас есть функция `isAdmin()`, которая возвращает `true`, если у посетителя есть права администратора. - -Можно создать декоратор `checkPermissionDecorator`, который добавляет в любую функцию проверку прав: - -Например, создадим декоратор `checkPermissionDecorator(f)`. Он будет возвращать обертку, которая передает вызов `f` в том случае, если у посетителя достаточно прав: - -```js -function checkPermissionDecorator(f) { - return function() { - if (isAdmin()) { - return f.apply(this, arguments); - } - alert( 'Недостаточно прав' ); - } -} -``` - -Использование декоратора: - -```js -//+ no-beautify -function save() { ... } - -save = checkPermissionDecorator(save); -// Теперь вызов функции save() проверяет права -``` - -## Итого - -Декоратор -- это обёртка над функцией, которая модифицирует её поведение. При этом основную работу по-прежнему выполняет функция. - -**Декораторы можно не только повторно использовать, но и комбинировать!** - -Это кардинально повышает их выразительную силу. Декораторы можно рассматривать как своего рода "фичи" или возможности, которые можно "нацепить" на любую функцию. Можно один, а можно несколько. - -Скажем, используя декораторы, описанные выше, можно добавить к функции возможности по проверке типов данных, замеру времени и проверке доступа буквально одной строкой, не залезая при этом в её код, то есть (!) не увеличивая его сложность. - -Предлагаю вашему вниманию задачи, которые помогут выяснить, насколько вы разобрались в декораторах. Далее в учебнике мы ещё встретимся с ними. - - - -[head] - -[/head] \ No newline at end of file diff --git a/1-js/6-objects-more/index.md b/1-js/6-objects-more/index.md deleted file mode 100644 index 94f1be86..00000000 --- a/1-js/6-objects-more/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Методы объектов и контекст вызова - -Начинаем изучать объектно-ориентированную разработку -- как работают объекты и функции, что такое контекст вызова и способы его передачи. \ No newline at end of file diff --git a/1-js/7-js-misc/1-class-instanceof/1-format-date-polymorphic/_js.view/solution.js b/1-js/7-js-misc/1-class-instanceof/1-format-date-polymorphic/_js.view/solution.js deleted file mode 100644 index 48113fa4..00000000 --- a/1-js/7-js-misc/1-class-instanceof/1-format-date-polymorphic/_js.view/solution.js +++ /dev/null @@ -1,32 +0,0 @@ -function formatDate(date) { - if (typeof date == 'number') { - // перевести секунды в миллисекунды и преобразовать к Date - date = new Date(date * 1000); - } else if (typeof date == 'string') { - // строка в стандартном формате автоматически будет разобрана в дату - date = new Date(date); - } else if (Array.isArray(date)) { - date = new Date(date[0], date[1], date[2]); - } - // преобразования для поддержки полиморфизма завершены, - // теперь мы работаем с датой (форматируем её) - - return date.toLocaleString("ru", {day: '2-digit', month: '2-digit', year: '2-digit'}); - - /* - // можно и вручную, если лень добавлят в старый IE поддержку локализации - var day = date.getDate(); - if (day < 10) day = '0' + day; - - var month = date.getMonth() + 1; - if (month < 10) month = '0' + month; - - // взять 2 последние цифры года - var year = date.getFullYear() % 100; - if (year < 10) year = '0' + year; - - var formattedDate = day + '.' + month + '.' + year; - - return formattedDate; - */ -} \ No newline at end of file diff --git a/1-js/7-js-misc/1-class-instanceof/1-format-date-polymorphic/_js.view/test.js b/1-js/7-js-misc/1-class-instanceof/1-format-date-polymorphic/_js.view/test.js deleted file mode 100644 index a458b62f..00000000 --- a/1-js/7-js-misc/1-class-instanceof/1-format-date-polymorphic/_js.view/test.js +++ /dev/null @@ -1,18 +0,0 @@ -describe("formatDate", function() { - it("читает дату вида гггг-мм-дд из строки", function() { - assert.equal(formatDate('2011-10-02'), "02.10.11"); - }); - - it("читает дату из числа 1234567890 (миллисекунды)", function() { - assert.equal(formatDate(1234567890), "14.02.09"); - }); - - it("читает дату из массива вида [гггг, м, д]", function() { - assert.equal(formatDate([2014, 0, 1]), "01.01.14"); - }); - - it("читает дату из объекта Date", function() { - assert.equal(formatDate(new Date(2014, 0, 1)), "01.01.14"); - }); - -}); \ No newline at end of file diff --git a/1-js/7-js-misc/1-class-instanceof/1-format-date-polymorphic/solution.md b/1-js/7-js-misc/1-class-instanceof/1-format-date-polymorphic/solution.md deleted file mode 100644 index 878033e9..00000000 --- a/1-js/7-js-misc/1-class-instanceof/1-format-date-polymorphic/solution.md +++ /dev/null @@ -1,15 +0,0 @@ -Для определения примитивного типа строка/число подойдет оператор [typeof](#type-typeof). - -Примеры его работы: - -```js -//+ run -alert( typeof 123 ); // "number" -alert( typeof "строка" ); // "string" -alert( typeof new Date() ); // "object" -alert( typeof [] ); // "object" -``` - -Оператор `typeof` не умеет различать разные типы объектов, они для него все на одно лицо: `"object"`. Поэтому он не сможет отличить `Date` от `Array`. - -Для отличия `Array` используем вызов `Array.isArray`. Если он неверен, значит у нас дата. \ No newline at end of file diff --git a/1-js/7-js-misc/1-class-instanceof/1-format-date-polymorphic/task.md b/1-js/7-js-misc/1-class-instanceof/1-format-date-polymorphic/task.md deleted file mode 100644 index 4d4176da..00000000 --- a/1-js/7-js-misc/1-class-instanceof/1-format-date-polymorphic/task.md +++ /dev/null @@ -1,26 +0,0 @@ -# Полиморфная функция formatDate - -[importance 5] - -Напишите функцию `formatDate(date)`, которая возвращает дату в формате `dd.mm.yy`. - -Ее первый аргумент должен содержать дату в одном из видов: -
          -
        1. Как объект `Date`.
        2. -
        3. Как строку, например `yyyy-mm-dd` или другую в стандартном формате даты.
        4. -
        5. Как число *секунд* с `01.01.1970`.
        6. -
        7. Как массив `[гггг, мм, дд]`, месяц начинается с нуля
        8. -
        -Для этого вам понадобится определить тип данных аргумента и, при необходимости, преобразовать входные данные в нужный формат. - -Пример работы: - -```js -function formatDate(date) { /* ваш код */ } - -alert( formatDate('2011-10-02') ); // 02.10.11 -alert( formatDate(1234567890) ); // 14.02.09 -alert( formatDate([2014, 0, 1]) ); // 01.01.14 -alert( formatDate(new Date(2014, 0, 1)) ); // 01.01.14 -``` - diff --git a/1-js/7-js-misc/1-class-instanceof/article.md b/1-js/7-js-misc/1-class-instanceof/article.md deleted file mode 100644 index 54b519b2..00000000 --- a/1-js/7-js-misc/1-class-instanceof/article.md +++ /dev/null @@ -1,252 +0,0 @@ -# Типы данных: [[Class]], instanceof и утки - -Время от времени бывает удобно создавать так называемые "полиморфные" функции, то есть такие, которые по-разному обрабатывают аргументы, в зависимости от их типа. Например, функция вывода может по-разному форматировать числа и даты. - -Для реализации такой возможности нужен способ определить тип переменной. - -## Оператор typeof - -Мы уже знакомы с простейшим способом -- оператором [typeof](#type-typeof). - -Оператор `typeof` надежно работает с примитивными типами, кроме `null`, а также с функциями. Он возвращает для них тип в виде строки: - -```js -//+ run no-beautify -alert( typeof 1 ); // 'number' -alert( typeof true ); // 'boolean' -alert( typeof "Текст" ); // 'string' -alert( typeof undefined ); // 'undefined' -alert( typeof null ); // 'object' (ошибка в языке) -alert( typeof alert ); // 'function' -``` - -...Но все объекты, включая массивы и даты для `typeof` -- на одно лицо, они имеют один тип `'object'`: - -```js -//+ run -alert( typeof {} ); // 'object' -alert( typeof [] ); // 'object' -alert( typeof new Date ); // 'object' -``` - -Поэтому различить их при помощи `typeof` нельзя, и в этом его основной недостаток. - -## Секретное свойство [[Class]] - -Для встроенных объектов есть одна "секретная" возможность узнать их тип, которая связана с методом `toString`. - -Во всех встроенных объектах есть специальное свойство `[[Class]]`, в котором хранится информация о его типе или конструкторе. - -Оно взято в квадратные скобки, так как это свойство -- внутреннее. Явно получить его нельзя, но можно прочитать его "в обход", воспользовавшись методом `toString` стандартного объекта `Object`. - -Его внутренняя реализация выводит `[[Class]]` в небольшом обрамлении, как `"[object значение]"`. - -Например: - -```js -//+ run -var toString = {}.toString; - -var arr = [1, 2]; -alert( toString.call(arr) ); // [object Array] - -var date = new Date; -alert( toString.call(date) ); // [object Date] - -var user = { name: "Вася" }; -alert( toString.call(user) ); // [object Object] -``` - -В первой строке мы взяли метод `toString`, принадлежащий именно стандартному объекту `{}`. Нам пришлось это сделать, так как у `Date` и `Array` -- свои собственные методы `toString`, которые работают иначе. - -Затем мы вызываем этот `toString` в контексте нужного объекта `obj`, и он возвращает его внутреннее, невидимое другими способами, свойство `[[Class]]`. - -**Для получения `[[Class]]` нужна именно внутренняя реализация `toString` стандартного объекта `Object`, другая не подойдёт.** - -К счастью, методы в JavaScript -- это всего лишь функции-свойства объекта, которые можно скопировать в переменную и применить на другом объекте через `call/apply`. Что мы и делаем для `{}.toString`. - -Метод также можно использовать с примитивами: - -```js -//+ run -alert( {}.toString.call(123) ); // [object Number] -alert( {}.toString.call("строка") ); // [object String] -``` - -[warn header="Вызов `{}.toString` в консоли может выдать ошибку"] -При тестировании кода в консоли вы можете обнаружить, что если ввести в командную строку `{}.toString.call(...)` -- будет ошибка. С другой стороны, вызов `alert( {}.toString... )` -- работает. - -Эта ошибка возникает потому, что фигурные скобки `{ }` в основном потоке кода интерпретируются как блок. Интерпретатор читает `{}.toString.call(...)` так: - -```js -//+ no-beautify -{ } // пустой блок кода -.toString.call(...) // а что это за точка в начале? не понимаю, ошибка! -``` - -Фигурные скобки считаются объектом, только если они находятся в контексте выражения. В частности, оборачивание в скобки `( {}.toString... )` тоже сработает нормально. -[/warn] - - -Для большего удобства можно сделать функцию `getClass`, которая будет возвращать только сам `[[Class]]`: - -```js -//+ run -function getClass(obj) { - return {}.toString.call(obj).slice(8, -1); -} - -alert( getClass(new Date) ); // Date -alert( getClass([1, 2, 3]) ); // Array -``` - -Заметим, что свойство `[[Class]]` есть и доступно для чтения указанным способом -- у всех *встроенных* объектов. Но его нет у объектов, которые создают *наши функции*. Точнее, оно есть, но равно всегда `"Object"`. - -Например: - -```js -//+ run -function User() {} - -var user = new User(); - -alert( {}.toString.call(user) ); // [object Object], не [object User] -``` - -Поэтому узнать тип таким образом можно только для встроенных объектов. - -## Метод Array.isArray() - -Для проверки на массивов есть специальный метод: `Array.isArray(arr)`. Он возвращает `true` только если `arr` -- массив: - -```js -//+ run -alert( Array.isArray([1,2,3]) ); // true -alert( Array.isArray("not array")); // false -``` - -Но этот метод -- единственный в своём роде. - -Других аналогичных, типа `Object.isObject`, `Date.isDate` -- нет. - - -## Оператор instanceof - -Оператор `instanceof` позволяет проверить, создан ли объект данной функцией, причём работает для любых функций -- как встроенных, так и наших. - -```js -//+ run -function User() {} - -var user = new User(); - -alert( user instanceof User ); // true -``` - -Таким образом, `instanceof`, в отличие от `[[Class]]` и `typeof` может помочь выяснить тип для новых объектов, созданных нашими конструкторами. - -Заметим, что оператор `instanceof` -- сложнее, чем кажется. Он учитывает наследование, которое мы пока не проходили, но скоро изучим, и затем вернёмся к `instanceof` в главе [](/instanceof). - - -## Утиная типизация - -Альтернативный подход к типу -- "утиная типизация", которая основана на одной известной пословице: *"If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck (who cares what it really is)"*. - -В переводе: *"Если это выглядит как утка, плавает как утка и крякает как утка, то, вероятно, это утка (какая разница, что это на самом деле)"*. - -Смысл утиной типизации -- в проверке необходимых методов и свойств. - -Например, мы можем проверить, что объект -- массив, не вызывая `Array.isArray`, а просто уточнив наличие важного для нас метода, например `splice`: - -```js -//+ run -var something = [1, 2, 3]; - -if (something.splice) { - alert( 'Это утка! То есть, массив!' ); -} -``` - -Обратите внимание -- в `if` мы не вызываем метод `something.splice()`, а пробуем получить само свойство `something.splice`. Для массивов оно всегда есть и является функцией, т.е. даст в логическом контексте `true`. - -Проверить на дату можно, определив наличие метода `getTime`: - -```js -//+ run -var x = new Date(); - -if (x.getTime) { - alert( 'Дата!' ); - alert( x.getTime() ); // работаем с датой -} -``` - -С виду такая проверка хрупка, ее можно "сломать", передав похожий объект с тем же методом. - -Но как раз в этом и есть смысл утиной типизации: если объект похож на дату, у него есть методы даты, то будем работать с ним как с датой (какая разница, что это на самом деле). - -То есть, мы намеренно позволяем передать в код нечто менее конкретное, чем определённый тип, чтобы сделать его более универсальным. - -[smart header="Проверка интерфейса"] -Если говорить словами "классического программирования", то "duck typing" -- это проверка реализации объектом требуемого интерфейса. Если реализует -- ок, используем его. Если нет -- значит это что-то другое. -[/smart] - - -## Пример полиморфной функции - -Пример полиморфной функции -- `sayHi(who)`, которая будет говорить "Привет" своему аргументу, причём если передан массив -- то "Привет" каждому: - -```js -//+ run -function sayHi(who) { - - if (Array.isArray(who)) { - who.forEach(sayHi); - } else { - alert( 'Привет, ' + who ); - } -} - -// Вызов с примитивным аргументом -sayHi("Вася"); // Привет, Вася - -// Вызов с массивом -sayHi(["Саша", "Петя"]); // Привет, Саша... Петя - -// Вызов с вложенными массивами - тоже работает! -sayHi(["Саша", "Петя", ["Маша", "Юля"]]); // Привет Саша..Петя..Маша..Юля -``` - -Проверку на массив в этом примере можно заменить на "утиную" -- нам ведь нужен только метод `forEach`: - -```js -//+ run -function sayHi(who) { - - if (who.forEach) { // если есть forEach - who.forEach(sayHi); // предполагаем, что он ведёт себя "как надо" - } else { - alert( 'Привет, ' + who ); - } -} -``` - -## Итого - -Для написания полиморфных (это удобно!) функций нам нужна проверка типов. - -
          -
        • Для примитивов с ней отлично справляется оператор `typeof`. - -У него две особенности: -
            -
          1. Он считает `null` объектом, это внутренняя ошибка в языке.
          2. -
          3. Для функций он возвращает `function`, по стандарту функция не считается базовым типом, но на практике это удобно и полезно.
          4. -
          -
        • -
        • Для встроенных объектов мы можем получить тип из скрытого свойства `[[Class]]`, при помощи вызова `{}.toString.call(obj).slice(8, -1)`. Не работает для конструкторов, которые объявлены нами. -
        • -
        • Оператор `obj instanceof Func` проверяет, создан ли объект `obj` функцией `Func`, работает для любых конструкторов. Более подробно мы разберём его в главе [](/instanceof).
        • -
        • И, наконец, зачастую достаточно проверить не сам тип, а просто наличие нужных свойств или методов. Это называется "утиная типизация".
        • -
        - diff --git a/1-js/7-js-misc/2-json/1-serialize-object/solution.md b/1-js/7-js-misc/2-json/1-serialize-object/solution.md deleted file mode 100644 index 0893b44e..00000000 --- a/1-js/7-js-misc/2-json/1-serialize-object/solution.md +++ /dev/null @@ -1,12 +0,0 @@ - - -```js -var leader = { - name: "Василий Иванович", - age: 35 -}; - -var leaderStr = JSON.stringify(leader); -leader = JSON.parse(leaderStr); -``` - diff --git a/1-js/7-js-misc/2-json/1-serialize-object/task.md b/1-js/7-js-misc/2-json/1-serialize-object/task.md deleted file mode 100644 index cc46554d..00000000 --- a/1-js/7-js-misc/2-json/1-serialize-object/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Превратите объект в JSON - -[importance 3] - -Превратите объект `leader` из примера ниже в JSON: - -```js -var leader = { - name: "Василий Иванович", - age: 35 -}; -``` - -После этого прочитайте получившуюся строку обратно в объект. diff --git a/1-js/7-js-misc/2-json/2-serialize-object-circular/solution.md b/1-js/7-js-misc/2-json/2-serialize-object-circular/solution.md deleted file mode 100644 index 0ec62c2b..00000000 --- a/1-js/7-js-misc/2-json/2-serialize-object-circular/solution.md +++ /dev/null @@ -1,64 +0,0 @@ -# Ответ на первый вопрос - -Обычный вызов `JSON.stringify(team)` выдаст ошибку, так как объекты `leader` и `soldier` внутри структуры ссылаются друг на друга. - -Формат JSON не предусматривает средств для хранения ссылок. - -# Варианты решения - -Чтобы превращать такие структуры в JSON, обычно используются два подхода: - -
          -
        1. Добавить в `team` свой код `toJSON`: - -```js -team.toJSON = function() { - /* свой код, который может создавать копию объекта без круговых ссылок и передавать управление JSON.stringify */ -} -``` - -При этом, конечно, понадобится и своя функция чтения из JSON, которая будет восстанавливать объект, а затем дополнять его круговыми ссылками. -
        2. -
        3. Можно учесть возможную проблему в самой структуре, используя вместо ссылок `id`. Как правило, это несложно, ведь на сервере у данных тоже есть идентификаторы. - -Изменённая структура может выглядеть так: - -```js -var leader = { - id: 12, - name: "Василий Иванович" -}; - -var soldier = { - id: 51, - name: "Петька" -}; - -*!* -// поменяли прямую ссылку на ID -leader.soldierId = 51; -soldier.leaderId = 12; -*/!* - -var team = { - 12: leader, - 51: soldier -}; -``` - -..Но действительно ли это решение будет оптимальным? Использовать структуру стало сложнее, и вряд ли это изменение стоит делать лишь из-за JSON. Вот если есть другие преимущества, тогда можно подумать. -
        4. -
        - -Универсальный вариант подхода, описанного выше -- это использование особой реализации JSON, которая не входит в стандарт и поддерживает расширенный формат для поддержки ссылок. - -Она, к примеру, есть во фреймворке Dojo. - -При вызове `dojox.json.ref.toJson(team)` будет создано следующее строковое представление: - -```js -//+ no-beautify -[{"name":"Василий Иванович","soldier":{"name":"Петька","leader":{"$ref":"#0"}}},{"$ref":"#0.soldier"}] -``` - -Метод разбора такой строки -- также свой: `dojox.json.ref.fromJson`. \ No newline at end of file diff --git a/1-js/7-js-misc/2-json/2-serialize-object-circular/task.md b/1-js/7-js-misc/2-json/2-serialize-object-circular/task.md deleted file mode 100644 index 7aa131ef..00000000 --- a/1-js/7-js-misc/2-json/2-serialize-object-circular/task.md +++ /dev/null @@ -1,26 +0,0 @@ -# Превратите объекты со ссылками в JSON - -[importance 3] - -Превратите объект `team` из примера ниже в JSON: - -```js -var leader = { - name: "Василий Иванович" -}; - -var soldier = { - name: "Петька" -}; - -// эти объекты ссылаются друг на друга! -leader.soldier = soldier; -soldier.leader = leader; - -var team = [leader, soldier]; -``` - -
          -
        1. Может ли это сделать прямой вызов `JSON.stringify(team)`? Если нет, то почему?
        2. -
        3. Какой подход вы бы предложили для чтения и восстановления таких объектов?
        4. -
        \ No newline at end of file diff --git a/1-js/7-js-misc/2-json/article.md b/1-js/7-js-misc/2-json/article.md deleted file mode 100644 index ad99b881..00000000 --- a/1-js/7-js-misc/2-json/article.md +++ /dev/null @@ -1,366 +0,0 @@ -# Формат JSON, метод toJSON - -В этой главе мы рассмотрим работу с форматом [JSON](http://ru.wikipedia.org/wiki/JSON), который используется для представления объектов в виде строки. - -Это один из наиболее удобных форматов данных при взаимодействии с JavaScript. Если нужно с сервера взять объект с данными и передать на клиенте, то в качестве промежуточного формата -- для передачи по сети, почти всегда используют именно его. - -В современных браузерах есть замечательные методы, знание тонкостей которых делает операции с JSON простыми и комфортными. - -[cut] - -## Формат JSON - -Данные в формате JSON ([RFC 4627](http://tools.ietf.org/html/rfc4627)) представляют собой: -
          -
        • JavaScript-объекты `{ ... }` или
        • -
        • Массивы `[ ... ]` или
        • -
        • Значения одного из типов: -
            -
          • строки в двойных кавычках,
          • -
          • число,
          • -
          • логическое значение `true`/`false`,
          • -
          • `null`.
          • -
          -
        • -
        - -Почти все языки программирования имеют библиотеки для преобразования объектов в формат JSON. - -Основные методы для работы с JSON в JavaScript -- это: -
          -
        • `JSON.parse` -- читает объекты из строки в формате JSON.
        • -
        • `JSON.stringify` -- превращает объекты в строку в формате JSON, используется, когда нужно из JavaScript передать данные по сети.
        • -
        - -## Метод JSON.parse - -Вызов `JSON.parse(str)` превратит строку с данными в формате JSON в JavaScript-объект/массив/значение. - -Например: - -```js -//+ run -var numbers = "[0, 1, 2, 3]"; - -numbers = JSON.parse(numbers); - -alert( numbers[1] ); // 1 -``` - -Или так: - -```js -//+ run -var user = '{ "name": "Вася", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }'; - -user = JSON.parse(user); - -alert( user.friends[1] ); // 1 -``` - -Данные могут быть сколь угодно сложными, объекты и массивы могут включать в себя другие объекты и массивы. Главное чтобы они соответствовали формату. - -[warn header="JSON-объекты ≠ JavaScript-объекты"] -Объекты в формате JSON похожи на обычные JavaScript-объекты, но отличаются от них более строгими требованиями к строкам -- они должны быть именно в двойных кавычках. - -В частности, первые два свойства объекта ниже -- некорректны: - -```js -{ - *!*name*/!*: "Вася", // ошибка: ключ name без кавычек! - "surname": *!*'Петров'*/!*,// ошибка: одинарные кавычки у значения 'Петров'! - "age": 35 // .. а тут всё в порядке. - "isAdmin": false // и тут тоже всё ок -} -``` - -Кроме того, в формате JSON не поддерживаются комментарии. Он предназначен только для передачи данных. - -Есть нестандартное расширение формата JSON, которое называется [JSON5](http://json5.org/) и как раз разрешает ключи без кавычек, комментарии и т.п, как в обычном JavaScript. На данном этапе, это отдельная библиотека. -[/warn] - -## Умный разбор: JSON.parse(str, reviver) - -Метод `JSON.parse` поддерживает и более сложные алгоритмы разбора. - -Например, мы получили с сервера объект с данными события `event`. - -Он выглядит так: - -```js -// title: название собятия, date: дата события -var str = '{"title":"Конференция","date":"2012-11-30T12:00:00.000Z"}'; -``` - -...И теперь нужно *восстановить* его, то есть превратить в JavaScript-объект. - -Попробуем вызвать для этого `JSON.parse`: - -```js -//+ run -var str = '{"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}'; - -var event = JSON.parse(str); - -*!* -alert( event.date.getDate() ); // ошибка! -*/!* -``` - -...Увы, ошибка! - -Дело в том, что значением `event.date` является строка, а отнюдь не объект `Date`. Откуда методу `JSON.parse` знать, что нужно превратить строку именно в дату? - -**Для интеллектуального восстановления из строки у `JSON.parse(str, reviver)` есть второй параметр `reviver`, который является функцией `function(key, value)`.** - -Если она указана, то в процессе чтения объекта из строки `JSON.parse` передаёт ей по очереди все создаваемые пары ключ-значение и может возвратить либо преобразованное значение, либо `undefined`, если его нужно пропустить. - -В данном случае мы можем создать правило, что ключ `date` всегда означает дату: - -```js -//+ run -// дата в строке - в формате UTC -var str = '{"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}'; - -*!* -var event = JSON.parse(str, function(key, value) { - if (key == 'date') return new Date(value); - return value; -}); -*/!* - -alert( event.date.getDate() ); // теперь сработает! -``` - -Кстати, эта возможность работает и для вложенных объектов тоже: - -```js -//+ run -var schedule = '{ \ - "events": [ \ - {"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}, \ - {"title":"День рождения","date":"2015-04-18T12:00:00.000Z"} \ - ]\ -}'; - -schedule = JSON.parse(schedule, function(key, value) { - if (key == 'date') return new Date(value); - return value; -}); - -*!* -alert( schedule.events[1].date.getDate() ); // сработает! -*/!* -``` - -## Сериализация, метод JSON.stringify - -Метод `JSON.stringify(value, replacer, space)` преобразует ("сериализует") значение в JSON-строку. - -Пример использования: - -```js -//+ run -var event = { - title: "Конференция", - date: "сегодня" -}; - -var str = JSON.stringify(event); -alert( str ); // {"title":"Конференция","date":"сегодня"} - -// Обратное преобразование. -event = JSON.parse(str); -``` - -**При сериализации объекта вызывается его метод `toJSON`.** - -Если такого метода нет -- перечисляются его свойства, кроме функций. - -Посмотрим это в примере посложнее: - -```js -//+ run -var room = { - number: 23, - occupy: function() { - alert( this.number ); - } -}; - -event = { - title: "Конференция", - date: new Date(Date.UTC(2014, 0, 1)), - room: room -}; - -alert( JSON.stringify(event) ); -/* - { - "title":"Конференция", - "date":"2014-01-01T00:00:00.000Z", // (1) - "room": {"number":23} // (2) - } -*/ -``` - -Обратим внимание на два момента: -
          -
        1. Дата превратилась в строку. Это не случайно: у всех дат есть встроенный метод `toJSON`. Его результат в данном случае -- строка в таймзоне UTC.
        2. -
        3. У объекта `room` нет метода `toJSON`. Поэтому он сериализуется перечислением свойств. - -Мы, конечно, могли бы добавить такой метод, тогда в итог попал бы его результат: - -```js -//+ run -var room = { - number: 23, -*!* - toJSON: function() { - return this.number; - } -*/!* -}; - -alert( JSON.stringify(room) ); // 23 -``` - -
        4. -
        - -### Исключение свойств - -Попытаемся преобразовать в JSON объект, содержащий ссылку на DOM. - -Например: - -```js -//+ run -var user = { - name: "Вася", - age: 25, - window: window -}; - -*!* -alert( JSON.stringify(user) ); // ошибка! -// TypeError: Converting circular structure to JSON (текст из Chrome) -*/!* -``` - -Произошла ошибка! В чём же дело, неужели некоторые объекты запрещены? Как видно из текста ошибки -- дело совсем в другом. Глобальный объект `window` -- сложная структура с кучей встроенных свойств и круговыми ссылками, поэтому его преобразовать невозможно. Да и нужно ли? - -**Во втором параметре `JSON.stringify(value, replacer)` можно указать массив свойств, которые подлежат сериализации.** - -Например: - -```js -//+ run -var user = { - name: "Вася", - age: 25, - window: window -}; - -*!* -alert( JSON.stringify(user, ["name", "age"]) ); -// {"name":"Вася","age":25} -*/!* -``` - -Для более сложных ситуаций вторым параметром можно передать функцию `function(key, value)`, которая возвращает сериализованное `value` либо `undefined`, если его не нужно включать в результат: - -```js -//+ run -var user = { - name: "Вася", - age: 25, - window: window -}; - -*!* -var str = JSON.stringify(user, function(key, value) { - if (key == 'window') return undefined; - return value; -}); -*/!* - -alert( str ); // {"name":"Вася","age":25} -``` - -В примере выше функция пропустит свойство с названием `window`. Для остальных она просто возвращает значение, передавая его стандартному алгоритму. А могла бы и как-то обработать. - -[smart header="Функция `replacer` работает рекурсивно"] -То есть, если объект содержит вложенные объекты, массивы и т.п., то все они пройдут через `replacer`. -[/smart] - -### Красивое форматирование - -В методе `JSON.stringify(value, replacer, space)` есть ещё третий параметр `space`. - -Если он является числом -- то уровни вложенности в JSON оформляются указанным количеством пробелов, если строкой -- вставляется эта строка. - -Например: - -```js -//+ run -var user = { - name: "Вася", - age: 25, - roles: { - isAdmin: false, - isEditor: true - } -}; - -*!* -var str = JSON.stringify(user, "", 4); -*/!* - -alert( str ); -/* Результат -- красиво сериализованный объект: -{ - "name": "Вася", - "age": 25, - "roles": { - "isAdmin": false, - "isEditor": true - } -} -*/ -``` - -## Итого - -
          -
        • JSON -- формат для представления объектов (и не только) в виде строки.
        • -
        • Методы [JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) и [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) позволяют интеллектуально преобразовать объект в строку и обратно.
        • -
        - - - -[head] - -[/head] \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/1-output-numbers-100ms/solution.md b/1-js/7-js-misc/3-setTimeout-setInterval/1-output-numbers-100ms/solution.md deleted file mode 100644 index f6e7fbcd..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/1-output-numbers-100ms/solution.md +++ /dev/null @@ -1,17 +0,0 @@ - - -```js -//+ run -function printNumbersInterval() { - var i = 1; - var timerId = setInterval(function() { - console.log(i); - if (i == 20) clearInterval(timerId); - i++; - }, 100); -} - -// вызов -printNumbersInterval(); -``` - diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/1-output-numbers-100ms/task.md b/1-js/7-js-misc/3-setTimeout-setInterval/1-output-numbers-100ms/task.md deleted file mode 100644 index dc2364f4..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/1-output-numbers-100ms/task.md +++ /dev/null @@ -1,21 +0,0 @@ -# Вывод чисел каждые 100мс - -[importance 5] - -Напишите функцию `printNumbersInterval()`, которая последовательно выводит в консоль числа от 1 до 20, с интервалом между числами 100мс. То есть, весь вывод должен занимать 2000мс, в течение которых каждые 100мс в консоли появляется очередное число. - -Нажмите на кнопку, открыв консоль, для демонстрации: - - - - -P.S. Функция должна использовать `setInterval`. \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/2-output-numbers-100ms-settimeout/solution.md b/1-js/7-js-misc/3-setTimeout-setInterval/2-output-numbers-100ms-settimeout/solution.md deleted file mode 100644 index bc23dd04..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/2-output-numbers-100ms-settimeout/solution.md +++ /dev/null @@ -1,17 +0,0 @@ - - -```js -//+ run -function printNumbersTimeout20_100() { - var i = 1; - var timerId = setTimeout(function go() { - console.log(i); - if (i < 20) setTimeout(go, 100); - i++; - }, 100); -} - -// вызов -printNumbersTimeout20_100(); -``` - diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/2-output-numbers-100ms-settimeout/task.md b/1-js/7-js-misc/3-setTimeout-setInterval/2-output-numbers-100ms-settimeout/task.md deleted file mode 100644 index 8334753f..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/2-output-numbers-100ms-settimeout/task.md +++ /dev/null @@ -1,5 +0,0 @@ -# Вывод чисел каждые 100мс, через setTimeout - -[importance 5] - -Сделайте то же самое, что в задаче [](/task/output-numbers-100ms), но с использованием рекурсивного `setTimeout` вместо `setInterval`. \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/3-highlight-tactics/solution.md b/1-js/7-js-misc/3-setTimeout-setInterval/3-highlight-tactics/solution.md deleted file mode 100644 index 13c70041..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/3-highlight-tactics/solution.md +++ /dev/null @@ -1,5 +0,0 @@ -**Нужно выбрать вариант 2, который гарантирует браузеру свободное время между выполнениями `highlight`.** - -Первый вариант может загрузить процессор на 100%, если `highlight` занимает время, близкое к 10мс или, тем более, большее чем 10мс, т.к. таймер не учитывает время выполнения функции. - -Что интересно, в обоих случаях браузер не будет выводить предупреждение о том, что скрипт занимает много времени. Но от 100% загрузки процессора возможны притормаживания других операций. В общем, это совсем не то, что мы хотим, поэтому вариант 2. \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/3-highlight-tactics/task.md b/1-js/7-js-misc/3-setTimeout-setInterval/3-highlight-tactics/task.md deleted file mode 100644 index afe1d6fd..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/3-highlight-tactics/task.md +++ /dev/null @@ -1,34 +0,0 @@ -# Для подсветки setInterval или setTimeout? - -[importance 5] - -Стоит задача: реализовать подсветку синтаксиса в длинном коде при помощи JavaScript, для онлайн-редактора кода. Это требует сложных вычислений, особенно загружает процессор генерация дополнительных элементов страницы, визуально осуществляющих подсветку. - -Поэтому решаем обрабатывать не весь код сразу, что привело бы к зависанию скрипта, а разбить работу на части: подсвечивать по 20 строк раз в 10мс. - -Как мы знаем, есть два варианта реализации такой подсветки: - -
          -
        1. Через `setInterval`, с остановкой по окончании работы: - -```js -timer = setInterval(function() { - if (есть еще что подсветить) highlight(); - else clearInterval(timer); -}, 10); -``` - -
        2. -
        3. Через рекурсивный `setTimeout`: - -```js -setTimeout(function go() { - highlight(); - if (есть еще что подсветить) setTimeout(go, 10); -}, 10); -``` - -
        4. -
        - -Какой из них стоит использовать? Почему? \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/4-settimeout-result/solution.md b/1-js/7-js-misc/3-setTimeout-setInterval/4-settimeout-result/solution.md deleted file mode 100644 index ea95474b..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/4-settimeout-result/solution.md +++ /dev/null @@ -1,8 +0,0 @@ -Ответы: -
          -
        • `alert` выведет `100000000`.
        • -
        • **3**, срабатывание будет после окончания работы `hardWork`.
        • -
        - - -Так будет потому, что вызов планируется на `100мс` от времени вызова `setTimeout`, но функция выполняется больше, чем `100мс`, поэтому к моменту ее окончания время уже подошло и отложенный вызов выполняется тут же. \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/4-settimeout-result/task.md b/1-js/7-js-misc/3-setTimeout-setInterval/4-settimeout-result/task.md deleted file mode 100644 index 8c2de36d..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/4-settimeout-result/task.md +++ /dev/null @@ -1,31 +0,0 @@ -# Что выведет setTimeout? - -[importance 5] - -В коде ниже запланирован запуск `setTimeout`, а затем запущена тяжёлая функция `hardWork`, выполнение которой занимает более долгое время, чем интервал до срабатывания таймера. - -Когда сработает `setTimeout`? Выберите нужный вариант: -
          -
        1. До выполнения `hardWork`.
        2. -
        3. Во время выполнения `hardWork`.
        4. -
        5. Сразу же по окончании `hardWork`.
        6. -
        7. Через 100мс после окончания `hardWork`.
        8. -
        - -Что выведет `alert` в коде ниже? - -```js -setTimeout(function() { - alert( i ); -}, 100); - -var i; - -function hardWork() { - // время выполнения этого кода >100мс, сам код неважен - for (i = 0; i < 1e8; i++) hardWork[i % 2] = i; -} - -hardWork(); -``` - diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/5-setinterval-result/solution.md b/1-js/7-js-misc/3-setTimeout-setInterval/5-setinterval-result/solution.md deleted file mode 100644 index 1fe34f89..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/5-setinterval-result/solution.md +++ /dev/null @@ -1,37 +0,0 @@ -Вызов `alert(i)` в `setTimeout` введет `100000001`. - -Можете проверить это запуском: - -```js -//+ run -var timer = setInterval(function() { - i++; -}, 10); - -setTimeout(function() { - clearInterval(timer); -*!* - alert( i ); // (*) -*/!* -}, 50); - -var i; - -function f() { - // точное время выполнения не играет роли - // здесь оно заведомо больше 100мс - for (i = 0; i < 1e8; i++) f[i % 2] = i; -} - -f(); -``` - -Правильный вариант срабатывания: **3** (сразу же по окончании `f` один раз). - -Планирование `setInterval` будет вызывать функцию каждые `10мс` после текущего времени. Но так как интерпретатор занят долгой функцией, то до конца ее работы никакого вызова не происходит. - -За время выполнения `f` может пройти время, на которое запланированы несколько вызовов `setInterval`, но в этом случае остается только один, т.е. накопления вызовов не происходит. Такова логика работы `setInterval`. - -После окончания текущего скрипта интерпретатор обращается к очереди запланированных вызовов, видит в ней `setInterval` и выполняет. А затем тут же выполняется `setTimeout`, очередь которого тут же подошла. - -Итого, как раз и видим, что `setInterval` выполнился ровно 1 раз по окончании работы функции. Такое поведение кросс-браузерно. \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/5-setinterval-result/task.md b/1-js/7-js-misc/3-setTimeout-setInterval/5-setinterval-result/task.md deleted file mode 100644 index 84461c2d..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/5-setinterval-result/task.md +++ /dev/null @@ -1,44 +0,0 @@ -# Что выведет после setInterval? - -[importance 5] - -В коде ниже запускается `setInterval` каждые 10мс, и через 50мс запланирована его отмена. - -После этого запущена тяжёлая функция `f`, выполнение которой (мы точно знаем) потребует более 100мс. - -Сработает ли `setInterval`, как и когда? - -Варианты: -
          -
        1. Да, несколько раз, *в процессе* выполнения `f`.
        2. -
        3. Да, несколько раз, *сразу после* выполнения `f`.
        4. -
        5. Да, один раз, *сразу после* выполнения `f`.
        6. -
        7. Нет, не сработает.
        8. -
        9. Может быть по-разному, как повезёт.
        10. -
        - -Что выведет `alert` в строке `(*)`? - -```js -var i; -var timer = setInterval(function() { // планируем setInterval каждые 10мс - i++; -}, 10); - -setTimeout(function() { // через 50мс - отмена setInterval - clearInterval(timer); -*!* - alert( i ); // (*) -*/!* -}, 50); - -// и запускаем тяжёлую функцию -function f() { - // точное время выполнения не играет роли - // здесь оно заведомо больше 100мс - for (i = 0; i < 1e8; i++) f[i % 2] = i; -} - -f(); -``` - diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/6-who-runs-faster/solution.md b/1-js/7-js-misc/3-setTimeout-setInterval/6-who-runs-faster/solution.md deleted file mode 100644 index 9d8afe0d..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/6-who-runs-faster/solution.md +++ /dev/null @@ -1,55 +0,0 @@ -Задача -- с небольшим "нюансом". - -Есть браузеры, в которых на время работы JavaScript таймер "застывает", например таков IE. В них количество шагов будет почти одинаковым, +-1. - -В других браузерах (Chrome) первый бегун будет быстрее. - -Создадим реальные объекты `Runner` и запустим их для проверки: - -```js -//+ run -function Runner() { - this.steps = 0; - - this.step = function() { - this.doSomethingHeavy(); - this.steps++; - }; - - function fib(n) { - return n <= 1 ? n : fib(n - 1) + fib(n - 2); - } - - this.doSomethingHeavy = function() { - for (var i = 0; i < 25; i++) { - this[i] = fib(i); - } - }; - -} - -var runner1 = new Runner(); -var runner2 = new Runner(); - -// запускаем бегунов -var t1 = setInterval(function() { - runner1.step(); -}, 15); - -var t2 = setTimeout(function go() { - runner2.step(); - t2 = setTimeout(go, 15); -}, 15); - -// кто сделает больше шагов? -setTimeout(function() { - clearInterval(t1); - clearTimeout(t2); - alert( runner1.steps ); - alert( runner2.steps ); -}, 5000); -``` - -Если бы в шаге `step()` не было вызова `doSomethingHeavy()`, то есть он бы не требовал времени, то количество шагов было бы почти равным. - -Но так как у нас шаг, всё же, что-то делает, и функция `doSomethingHeavy()` специально написана таким образом, что она требует (небольшого) времени, то первый бегун успеет сделать больше шагов. Ведь в `setTimeout` пауза `15` мс будет *между* шагами, а `setInterval` шагает равномерно, каждые `15` мс. Получается чаще. \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/6-who-runs-faster/task.md b/1-js/7-js-misc/3-setTimeout-setInterval/6-who-runs-faster/task.md deleted file mode 100644 index 85d20a37..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/6-who-runs-faster/task.md +++ /dev/null @@ -1,35 +0,0 @@ -# Кто быстрее? - -[importance 5] - -Есть два бегуна: - -```js -var runner1 = new Runner(); -var runner2 = new Runner(); -``` - -У каждого есть метод `step()`, который делает шаг, увеличивая свойство `steps`. - -Конкретный код метода `step()` не имеет значения, важно лишь что шаг делается не мгновенно, он требует небольшого времени. - -Если запустить первого бегуна через `setInterval`, а второго -- через вложенный `setTimeout` -- какой сделает больше шагов за 5 секунд? - -```js -// первый? -setInterval(function() { - runner1.step(); -}, 15); - -// или второй? -setTimeout(function go() { - runner2.step(); - setTimeout(go, 15); -}, 15); - -setTimeout(function() { - alert( runner1.steps ); - alert( runner2.steps ); -}, 5000); -``` - diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/7-delay/_js.view/solution.js b/1-js/7-js-misc/3-setTimeout-setInterval/7-delay/_js.view/solution.js deleted file mode 100644 index 62271941..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/7-delay/_js.view/solution.js +++ /dev/null @@ -1,12 +0,0 @@ -function delay(f, ms) { - - return function() { - var savedThis = this; - var savedArgs = arguments; - - setTimeout(function() { - f.apply(savedThis, savedArgs); - }, ms); - }; - -} \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/7-delay/_js.view/test.js b/1-js/7-js-misc/3-setTimeout-setInterval/7-delay/_js.view/test.js deleted file mode 100644 index 964ccf60..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/7-delay/_js.view/test.js +++ /dev/null @@ -1,46 +0,0 @@ -describe("delay", function() { - before(function() { - this.clock = sinon.useFakeTimers(); - }); - - after(function() { - this.clock.restore(); - }); - - it("вызывает функцию через указанный таймаут", function() { - var start = Date.now(); - - function f(x) { - assert.equal(Date.now() - start, 1000); - } - f = sinon.spy(f); - - var f1000 = delay(f, 1000); - f1000("test"); - this.clock.tick(2000); - assert(f.calledOnce, 'calledOnce check fails'); - }); - - it("передаёт аргументы и контекст", function() { - var start = Date.now(); - var user = { - sayHi: function(phrase, who) { - assert.equal(this, user); - assert.equal(phrase, "Привет"); - assert.equal(who, "Вася"); - assert.equal(Date.now() - start, 1500); - } - }; - - user.sayHi = sinon.spy(user.sayHi); - - var spy = user.sayHi; - user.sayHi = delay(user.sayHi, 1500); - - user.sayHi("Привет", "Вася"); - - this.clock.tick(2000); - - assert(spy.calledOnce, 'проверка calledOnce не сработала'); - }); -}); \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/7-delay/solution.md b/1-js/7-js-misc/3-setTimeout-setInterval/7-delay/solution.md deleted file mode 100644 index 2044ff9b..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/7-delay/solution.md +++ /dev/null @@ -1,46 +0,0 @@ - - -```js -//+ run -function delay(f, ms) { - -*!* - return function() { - var savedThis = this; - var savedArgs = arguments; - - setTimeout(function() { - f.apply(savedThis, savedArgs); - }, ms); - }; -*/!* - -} - -function f(x) { - alert( x ); -} - -var f1000 = delay(f, 1000); -var f1500 = delay(f, 1500); - -f1000("тест"); // выведет "тест" через 1000 миллисекунд -f1500("тест2"); // выведет "тест2" через 1500 миллисекунд -``` - -Обратим внимание на то, как работает обёртка: - -```js -return function() { - var savedThis = this; - var savedArgs = arguments; - - setTimeout(function() { - f.apply(savedThis, savedArgs); - }, ms); -}; -``` - -Именно обёртка возвращается декоратором `delay` и будет вызвана. Чтобы передать аргумент и контекст функции, вызываемой через `ms` миллисекунд, они копируются в локальные переменные `savedThis` и `savedArgs`. - -Это один из самых простых, и в то же время удобных способов передать что-либо в функцию, вызываемую через `setTimeout`. \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/7-delay/task.md b/1-js/7-js-misc/3-setTimeout-setInterval/7-delay/task.md deleted file mode 100644 index f198d7b9..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/7-delay/task.md +++ /dev/null @@ -1,23 +0,0 @@ -# Функция-задержка - -[importance 5] - -Напишите функцию `delay(f, ms)`, которая возвращает обёртку вокруг `f`, задерживающую вызов на `ms` миллисекунд. - -Например: - -```js -function f(x) { - alert( x ); -} - -var f1000 = delay(f, 1000); -var f1500 = delay(f, 1500); - -f1000("тест"); // выведет "тест" через 1000 миллисекунд -f1500("тест2"); // выведет "тест2" через 1500 миллисекунд -``` - -Упрощённо можно сказать, что `delay` возвращает "задержанный на `ms`" вариант `f`. - -В примере выше у функции только один аргумент, но `delay` должна быть универсальной: передавать любое количество аргументов и контекст `this`. \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/8-debounce/_js.view/solution.js b/1-js/7-js-misc/3-setTimeout-setInterval/8-debounce/_js.view/solution.js deleted file mode 100644 index e52efb87..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/8-debounce/_js.view/solution.js +++ /dev/null @@ -1,19 +0,0 @@ -function debounce(f, ms) { - - var state = null; - - var COOLDOWN = 1; - - return function() { - if (state) return; - - f.apply(this, arguments); - - state = COOLDOWN; - - setTimeout(function() { - state = null - }, ms); - } - -} \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/8-debounce/_js.view/test.js b/1-js/7-js-misc/3-setTimeout-setInterval/8-debounce/_js.view/test.js deleted file mode 100644 index 860cd6dd..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/8-debounce/_js.view/test.js +++ /dev/null @@ -1,47 +0,0 @@ -describe("debounce", function() { - before(function() { - this.clock = sinon.useFakeTimers(); - }); - - after(function() { - this.clock.restore(); - }); - - it("вызывает функцию не чаще чем раз в ms миллисекунд", function() { - var log = ''; - - function f(a) { - log += a; - } - - f = debounce(f, 1000); - - f(1); // выполнится сразу же - f(2); // игнор - - setTimeout(function() { - f(3) - }, 100); // игнор (рановато) - setTimeout(function() { - f(4) - }, 1100); // выполнится (таймаут прошёл) - setTimeout(function() { - f(5) - }, 1500); // игнор - - this.clock.tick(5000); - assert.equal(log, "14"); - }); - - it("сохраняет контекст вызова", function() { - var obj = { - f: function() { - assert.equal(this, obj); - } - }; - - obj.f = debounce(obj.f, 1000); - obj.f("test"); - }); - -}); \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/8-debounce/solution.md b/1-js/7-js-misc/3-setTimeout-setInterval/8-debounce/solution.md deleted file mode 100644 index d5fc55e1..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/8-debounce/solution.md +++ /dev/null @@ -1,38 +0,0 @@ - - -```js -//+ run no-beautify -function debounce(f, ms) { - - var state = null; - - var COOLDOWN = 1; - - return function() { - if (state) return; - - f.apply(this, arguments); - - state = COOLDOWN; - - setTimeout(function() { state = null }, ms); - } - -} - -function f(x) { alert(x) } -var f = debounce(f, 1000); - -f(1); // 1, выполнится сразу же -f(2); // игнор - -setTimeout( function() { f(3) }, 100); // игнор (прошло только 100мс) -setTimeout( function() { f(4) }, 1100); // 4, выполнится -setTimeout( function() { f(5) }, 1500); // игнор -``` - -Вызов `debounce` возвращает функцию-обёртку. Все необходимые данные для неё хранятся в замыкании. - -При вызове ставится таймер и состояние `state` меняется на константу `COOLDOWN` ("в процессе охлаждения"). - -Последующие вызовы игнорируются, пока таймер не обнулит состояние. \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/8-debounce/task.md b/1-js/7-js-misc/3-setTimeout-setInterval/8-debounce/task.md deleted file mode 100644 index 31b9f540..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/8-debounce/task.md +++ /dev/null @@ -1,25 +0,0 @@ -# Вызов не чаще чем в N миллисекунд - -[importance 5] - -Напишите функцию `debounce(f, ms)`, которая возвращает обёртку, которая передаёт вызов `f` не чаще, чем раз в `ms` миллисекунд. - -"Лишние" вызовы игнорируются. Все аргументы и контекст -- передаются. - -Например: - -```js -//+ no-beautify -function f() { ... } - -var f = debounce(f, 1000); - -f(1); // выполнится сразу же -f(2); // игнор - -setTimeout( function() { f(3) }, 100); // игнор (прошло только 100мс) -setTimeout( function() { f(4) }, 1100); // выполнится -setTimeout( function() { f(5) }, 1500); // игнор -``` - -Упрощённо можно сказать, что `debounce` возвращает вариант `f`, срабатывающий не чаще чем раз в `ms` миллисекунд. \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/9-throttle/_js.view/solution.js b/1-js/7-js-misc/3-setTimeout-setInterval/9-throttle/_js.view/solution.js deleted file mode 100644 index a8011c6b..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/9-throttle/_js.view/solution.js +++ /dev/null @@ -1,29 +0,0 @@ -function throttle(func, ms) { - - var isThrottled = false, - savedArgs, - savedThis; - - function wrapper() { - - if (isThrottled) { - savedArgs = arguments; - savedThis = this; - return; - } - - func.apply(this, arguments); - - isThrottled = true; - - setTimeout(function() { - isThrottled = false; - if (savedArgs) { - wrapper.apply(savedThis, savedArgs); - savedArgs = savedThis = null; - } - }, ms); - } - - return wrapper; -} \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/9-throttle/_js.view/test.js b/1-js/7-js-misc/3-setTimeout-setInterval/9-throttle/_js.view/test.js deleted file mode 100644 index 1627e857..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/9-throttle/_js.view/test.js +++ /dev/null @@ -1,47 +0,0 @@ -describe("throttle(f, 1000)", function() { - var f1000; - var log = ""; - - function f(a) { - log += a; - } - - before(function() { - f1000 = throttle(f, 1000); - this.clock = sinon.useFakeTimers(); - }); - - it("первый вызов срабатывает тут же", function() { - f1000(1); // такой вызов должен сработать тут же - assert.equal(log, "1"); - }); - - it("тормозит второе срабатывание до 1000мс", function() { - f1000(2); // (тормозим, не прошло 1000мс) - f1000(3); // (тормозим, не прошло 1000мс) - // через 1000 мс запланирован вызов с последним аргументом - - assert.equal(log, "1"); // пока что сработал только первый вызов - - this.clock.tick(1000); // прошло 1000мс времени - assert.equal(log, "13"); // log==13, т.к. сработал вызов f1000(3) - }); - - it("тормозит третье срабатывание до 1000мс после второго", function() { - this.clock.tick(100); - f1000(4); // (тормозим, с последнего вызова прошло 100мс - менее 1000мс) - this.clock.tick(100); - f1000(5); // (тормозим, с последнего вызова прошло 200мс - менее 1000мс) - this.clock.tick(700); - f1000(6); // (тормозим, с последнего вызова прошло 900мс - менее 1000мс) - - this.clock.tick(100); // сработал вызов с 6 - - assert.equal(log, "136"); - }); - - after(function() { - this.clock.restore(); - }); - -}); \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/9-throttle/solution.md b/1-js/7-js-misc/3-setTimeout-setInterval/9-throttle/solution.md deleted file mode 100644 index 306f2977..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/9-throttle/solution.md +++ /dev/null @@ -1,42 +0,0 @@ - - -```js -function throttle(func, ms) { - - var isThrottled = false, - savedArgs, - savedThis; - - function wrapper() { - - if (isThrottled) { // (2) - savedArgs = arguments; - savedThis = this; - return; - } - - func.apply(this, arguments); // (1) - - isThrottled = true; - - setTimeout(function() { - isThrottled = false; // (3) - if (savedArgs) { - wrapper.apply(savedThis, savedArgs); - savedArgs = savedThis = null; - } - }, ms); - } - - return wrapper; -} -``` - -Шаги работы этой функции: -
          -
        1. Декоратор `throttle` возвращает функцию-обёртку `wrapper`, которая при первом вызове запускает `func` и переходит в состояние "паузы" (`isThrottled = true`).
        2. -
        3. В этом состоянии все новые вызовы запоминаются в замыкании через `savedArgs/savedThis`. Обратим внимание, что и контекст вызова и аргументы для нас одинаково важны и запоминаются одновременно. Только зная и то и другое, можно воспроизвести вызов правильно.
        4. -
        5. Далее, когда пройдёт таймаут `ms` миллисекунд -- пауза будет снята, а `wrapper` -- запущен с последними аргументами и контекстом (если во время паузы были вызовы).
        6. -
        - -Шаг `(3)` запускает именно не саму функцию, а снова `wrapper`, так как необходимо не только выполнить `func`, но и снова поставить выполнение на паузу. Получается последовательность "вызов - пауза.. вызов - пауза .. вызов - пауза ...", каждое выполнение в обязательном порядке сопровождается паузой после него. Это удобно описывается рекурсией. diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/9-throttle/task.md b/1-js/7-js-misc/3-setTimeout-setInterval/9-throttle/task.md deleted file mode 100644 index aaf5956b..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/9-throttle/task.md +++ /dev/null @@ -1,52 +0,0 @@ -# Тормозилка - -[importance 5] - -Напишите функцию `throttle(f, ms)` -- "тормозилку", которая возвращает обёртку, передающую вызов `f` не чаще, чем раз в `ms` миллисекунд. - -**У этой функции должно быть важное существенное отличие от `debounce`:** если игнорируемый вызов оказался последним, т.е. после него до окончания задержки ничего нет -- то он выполнится. - -Чтобы лучше понять, откуда взялось это требование, и как `throttle` должна работать -- разберём реальное применение, на которое и ориентирована эта задача. - -**Например, нужно обрабатывать передвижения мыши.** - -В JavaScript это делается функцией, которая будет запускаться при каждом микро-передвижении мыши и получать координаты курсора. По мере того, как мышь двигается, эта функция может запускаться очень часто, может быть 100 раз в секунду (каждые 10мс). - -**Функция обработки передвижения должна обновлять некую информацию на странице.** - -При этом обновление -- слишком "тяжёлый" процесс, чтобы делать его при каждом микро-передвижении. Имеет смысл делать его раз в 100мс, не чаще. - -Пусть функция, которая осуществляет это обновление по передвижению, называется `onmousemove`. - -Вызов `throttle(onmousemove, 100)`, по сути, предназначен для того, чтобы "притормаживать" обработку `onmousemove`. Технически, он должен возвращать обёртку, которая передаёт все вызовы `onmousemove`, но не чаще чем раз в 100мс. - -**При этом промежуточные движения можно игнорировать, но мышь в конце концов где-то остановится. И это последнее, итоговое положение мыши обязательно нужно обработать!** - -Визуально это даст следующую картину обработки перемещений мыши: -
          -
        1. Первое обновление произойдёт сразу (это важно, посетитель тут же видит реакцию на своё действие).
        2. -
        3. Дальше может быть много вызовов (микро-передвижений) с разными координатами, но пока не пройдёт 100мс -- ничего не будет.
        4. -
        5. По истечении 100мс -- опять обновление, с последними координатами. Промежуточные микро-передвижения игнорированы.
        6. -
        7. В конце концов мышь где-то остановится, обновление по окончании очередной паузы 100мс сработает с последними координатами.
        8. -
        - -Ещё раз заметим -- задача из реальной жизни, и в ней принципиально важно, что *последнее* передвижение обрабатывается. Пользователь должен увидеть, где остановил мышь. - -Пример использования: - -```js -var f = function(a) { - console.log(a) -}; - -// затормозить функцию до одного раза в 1000 мс -var f1000 = throttle(f, 1000); - -f1000(1); // выведет 1 -f1000(2); // (тормозим, не прошло 1000мс) -f1000(3); // (тормозим, не прошло 1000мс) - -// когда пройдёт 1000мс... -// выведет 3, промежуточное значение 2 игнорируется -``` - diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/article.md b/1-js/7-js-misc/3-setTimeout-setInterval/article.md deleted file mode 100644 index a85c0550..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/article.md +++ /dev/null @@ -1,326 +0,0 @@ -# setTimeout и setInterval - -Почти все реализации JavaScript имеют внутренний таймер-планировщик, который позволяет задавать вызов функции через заданный период времени. - -В частности, эта возможность поддерживается в браузерах и в сервере Node.JS. - -[cut] -## setTimeout - -Синтаксис: - -```js -var timerId = setTimeout(func / code, delay[, arg1, arg2...]) -``` - -Параметры: - -
        -
        `func/code`
        -
        Функция или строка кода для исполнения. -Строка поддерживается для совместимости, использовать её не рекомендуется.
        -
        `delay`
        -
        Задержка в милисекундах, 1000 милисекунд равны 1 секунде.
        -
        `arg1`, `arg2`...
        -
        Аргументы, которые нужно передать функции. Не поддерживаются в IE9-.
        -
        - -Исполнение функции произойдёт спустя время, указанное в параметре `delay`. - -Например, следующий код вызовет `func()` через одну секунду: - -```js -//+ run -function func() { - alert( 'Привет' ); -} - -*!* -setTimeout(func, 1000); -*/!* -``` - -С передачей аргументов (не сработает в IE9-): - -```js -//+ run -function func(phrase, who) { - alert( phrase + ', ' + who ); -} - -*!* -setTimeout(func, 1000, "Привет", "Вася"); // Привет, Вася -*/!* -``` - -Если первый аргумент является строкой, то интерпретатор создаёт анонимную функцию из этой строки. - -То есть такая запись тоже сработает: - -```js -//+ run no-beautify -setTimeout("alert('Привет')", 1000); -``` - -Однако, использование строк не рекомендуется, так как они могут вызвать проблемы при минимизации кода, и, вообще, сама возможность использовать строку сохраняется лишь для совместимости. - -Вместо них используйте анонимные функции, вот так: - -```js -//+ run no-beautify -setTimeout(function() { alert('Привет') }, 1000); -``` - -### Отмена исполнения clearTimeout - -Функция `setTimeout` возвращает числовой идентификатор таймера `timerId`, который можно использовать для отмены действия. - -Синтаксис: - -```js -var timerId = setTimeout(...); -clearTimeout(timerId); -``` - -В следующем примере мы ставим таймаут, а затем удаляем (передумали). В результате ничего не происходит. - -```js -//+ run no-beautify -var timerId = setTimeout(function() { alert(1) }, 1000); -alert(timerId); // число - идентификатор таймера - -clearTimeout(timerId); -alert(timerId); // всё ещё число, оно не обнуляется после отмены -``` - -Как видно из `alert`, в браузере идентификатор таймера является обычным числом. Другие JavaScript-окружения, например Node.JS, могут возвращать объект таймера, с дополнительными методами. - -**Такие разночтения вполне соответствуют стандарту просто потому, что в спецификации JavaScript про таймеры нет ни слова.** - -Таймеры -- это надстройка над JavaScript, которая описана в [секции Timers](http://www.w3.org/TR/html5/webappapis.html#timers) стандарта HTML5 для браузеров и в [документации к Node.JS](http://nodejs.org/docs/latest/api/timers.html) -- для сервера. - -## setInterval - -Метод `setInterval` имеет синтаксис, аналогичный `setTimeout`. - -```js -var timerId = setInterval(func / code, delay[, arg1, arg2...]) -``` - -Смысл аргументов -- тот же самый. Но, в отличие от `setTimeout`, он запускает выполнение функции не один раз, а регулярно повторяет её через указанный интервал времени. Остановить исполнение можно вызовом `clearInterval(timerId)`. - -Следующий пример при запуске станет выводить сообщение каждые две секунды, пока не пройдёт 5 секунд: - -```js -//+ run -// начать повторы с интервалом 2 сек -var timerId = setInterval(function() { - alert( "тик" ); -}, 2000); - -// через 5 сек остановить повторы -setTimeout(function() { - clearInterval(timerId); - alert( 'стоп' ); -}, 5000); -``` - -[smart header="Модальные окна замораживают время в Chrome/Opera/Safari"] -Что будет, если долго не жать `OK` на появившемся `alert`? Это зависит от браузера. - -В браузерах Chrome, Opera и Safari внутренний таймер "заморожен" во время показа `alert/confirm/prompt`. А вот в IE и Firefox внутренний таймер продолжит идти. - -Поэтому, если закрыть `alert` после небольшой паузы, то в Firefox/IE следующий `alert` будет показан сразу же (время подошло), а в Chrome/Opera/Safari -- только через 2 секунды после закрытия. -[/smart] - - -### Рекурсивный setTimeout - -Важная альтернатива `setInterval` -- рекурсивный `setTimeout`: - -```js -/** вместо: -var timerId = setInterval(function() { - alert( "тик" ); -}, 2000); -*/ - -var timerId = setTimeout(function tick() { - alert( "тик" ); -*!* - timerId = setTimeout(tick, 2000); -*/!* -}, 2000); -``` - -В коде выше следующее выполнение планируется сразу после окончания предыдущего. - -**Рекурсивный `setTimeout` -- более гибкий метод тайминга, чем `setInterval`, так как время до следующего выполнения можно запланировать по-разному, в зависимости от результатов текущего.** - -Например, у нас есть сервис, который в 5 секунд опрашивает сервер на предмет новых данных. В случае, если сервер перегружен, можно увеличивать интервал опроса до 10, 20, 60 секунд... А потом вернуть обратно, когда всё нормализуется. - -Если у нас регулярно проходят грузящие процессор задачи, то мы можем оценивать время, потраченное на их выполнение, и планировать следующий запуск раньше или позже. - -**Рекурсивный `setTimeout` гарантирует паузу между вызовами, `setInterval` -- нет.** - -Давайте сравним два кода. Первый использует `setInterval`: - -```js -var i = 1; -setInterval(function() { - func(i); -}, 100); -``` - -Второй использует рекурсивный `setTimeout`: - -```js -var i = 1; -setTimeout(function run() { - func(i); - setTimeout(run, 100); -}, 100); -``` - -При `setInterval` внутренний таймер будет срабатывать чётко каждые `100` мс и вызывать `func(i)`: - - - -Вы обратили внимание?... - -**Реальная пауза между вызовами `func` при `setInterval` меньше, чем указана в коде!** - -Это естественно, ведь время работы функции никак не учитывается, оно "съедает" часть интервала. - -Возможно и такое что `func` оказалась сложнее, чем мы рассчитывали и выполнялась дольше, чем 100мс. - -В этом случае интерпретатор будет ждать, пока функция завершится, затем проверит таймер и, если время вызова `setInterval` уже подошло (или прошло), то следующий вызов произойдёт *сразу же*. - -**Если функция и выполняется дольше, чем пауза `setInterval`, то вызовы будут происходить вообще без перерыва.** - -Исключением является IE, в котором таймер "застывает" во время выполнения JavaScript. - -А так будет выглядеть картинка с рекурсивным `setTimeout`: - - - -**При рекурсивном `setTimeout` задержка всегда фиксирована и равна 100мс.** - -Это происходит потому, что каждый новый запуск планируется только после окончания текущего. - -[smart header="Управление памятью"] -Сборщик мусора в JavaScript не чистит функции, назначенные в таймерах, пока таймеры актуальны. - -При передаче функции в `setInterval/setTimeout` создаётся внутренняя ссылка на неё, через которую браузер её будет запускать, и которая препятствует удалению из памяти, даже если функция анонимна. - -```js -// Функция будет жить в памяти, пока не сработал (или не был очищен) таймер -setTimeout(function() {}, 100); -``` - -
          -
        • Для `setTimeout` -- внутренняя ссылка исчезнет после исполнения функции.
        • -
        • Для `setInterval` -- ссылка исчезнет при очистке таймера.
        • -
        - -Так как функция также тянет за собой всё замыкание, то ставшие неактуальными, но не отменённые `setInterval` могут приводить к излишним тратам памяти. -[/smart] - - -## Минимальная задержка таймера - -У браузерного таймера есть минимальная возможная задержка. Она меняется от примерно нуля до 4мс в современных браузерах. В более старых она может быть больше и достигать 15мс. - -По [стандарту](http://www.w3.org/TR/html5/webappapis.html#timers), минимальная задержка составляет 4мс. Так что нет разницы между `setTimeout(..,1)` и `setTimeout(..,4)`. - -Посмотреть минимальное разрешение "вживую" можно на следующем примере. - -**В примере ниже каждая полоска удлиняется вызовом `setInterval` с указанной на ней задержкой -- от 0мс (сверху) до 20мс (внизу).** - -Позапускайте его в различных браузерах. Вы заметите, что несколько первых полосок анимируются с одинаковой скоростью. Это как раз потому, что слишком маленькие задержки таймер не различает. - -[iframe border="1" src="setinterval-anim" link edit] - -[warn] -В Internet Explorer, нулевая задержка `setInterval(.., 0)` не сработает. Это касается именно `setInterval`, т.е. `setTimeout(.., 0)` работает нормально. -[/warn] - -[smart header="Откуда взялись эти 4мс?"] -Почему минимальная задержка -- 4мс, а не 1мс? Зачем она вообще существует? - -Это -- "привет" от прошлого. Браузер Chrome как-то пытался убрать минимальную задержку в своих ранних версиях, но оказалось, что существуют сайты, которые используют `setTimeout(..,0)` рекурсивно, создавая тем самым "асинхронный цикл". И, если задержку совсем убрать, то будет 100% загрузка процессора, такой сайт "подвесит" браузер. - -Поэтому, чтобы не ломать существующие скрипты, решили сделать задержку. По возможности, небольшую. На время создания стандарта оптимальным числом показалось 4мс. -[/smart] - -## Реальная частота срабатывания - -В ряде ситуаций таймер будет срабатывать реже, чем обычно. Задержка между вызовами `setInterval(..., 4)` может быть не 4мс, а 30мс или даже 1000мс. - -
          -
        • Большинство браузеров (десктопных в первую очередь) продолжают выполнять `setTimeout/setInterval`, даже если вкладка неактивна. - -При этом ряд из них (Chrome, FF, IE10) снижают минимальную частоту таймера, до 1 раза в секунду. Получается, что в "фоновой" вкладке будет срабатывать таймер, но редко.
        • -
        • При работе от батареи, в ноутбуке -- браузеры тоже могут снижать частоту, чтобы реже выполнять код и экономить заряд батареи. Особенно этим известен IE. Снижение может достигать нескольких раз, в зависимости от настроек.
        • -
        • При слишком большой загрузке процессора JavaScript может не успевать обрабатывать таймеры вовремя. При этом некоторые запуски `setInterval` будут пропущены.
        • -
        - -**Вывод: на частоту 4мс стоит ориентироваться, но не стоит рассчитывать.** - -[online] -Посмотрим снижение частоты в действии на небольшом примере. - - -При клике на кнопку ниже запускается `setInterval(..., 90)`, который выводит список интервалов времени между 25 последними срабатываниями таймера. Запустите его. Перейдите на другую вкладку и вернитесь. - -
        - - - - - - -Если ваш браузер увеличивает таймаут при фоновом выполнении вкладки, то вы увидите увеличенные интервалы, помеченные красным. - -Кроме того, вы заметите, что таймер не является идеально точным ;) -[/online] - -## Разбивка долгих скриптов - -Нулевой или небольшой таймаут также используют, чтобы разорвать поток выполнения "тяжелых" скриптов. - -Например, скрипт для подсветки синтаксиса должен проанализировать код, создать много цветных элементов для подсветки и добавить их в документ -- на большом файле это займёт много времени, браузер может даже подвиснуть, что неприемлемо. - -Для того, чтобы этого избежать, сложная задача разбивается на части, выполнение каждой части запускается через мини-интервал после предыдущей, чтобы дать браузеру время. - -Например, осуществляется анализ и подсветка первых 100 строк, затем через 20 мс -- следующие 100 строк и так далее. При этом можно подстраиваться под CPU посетителя: замерять время на анализ 100 строк и, если процессор хороший, то в следующий раз обработать 200 строк, а если плохой -- то 50. В итоге подсветка будет работать с адекватной быстротой и без тормозов на любых текстах и компьютерах. - -## Итого - -
          -
        • Методы `setInterval(func, delay)` и `setTimeout(func, delay)` позволяют запускать `func` регулярно/один раз через `delay` миллисекунд.
        • -
        • Оба метода возвращают идентификатор таймера. Его используют для остановки выполнения вызовом `clearInterval/clearTimeout`.
        • -
        • В случаях, когда нужно гарантировать задержку между регулярными вызовами или гибко её менять, вместо `setInterval` используют рекурсивный `setTimeout`.
        • -
        • Минимальная задержка по стандарту составляет `4мс`. Браузеры соблюдают этот стандарт, но некоторые другие среды для выполнения JS, например Node.JS, могут предоставить и меньше задержки.
        • -
        • В реальности срабатывания таймера могут быть гораздо реже, чем назначено, например если процессор перегружен, вкладка находится в фоновом режиме, ноутбук работает от батареи или по какой-то иной причине.
        • - -Браузерных особенностей почти нет, разве что вызов `setInterval(..., 0)` с нулевой задержкой в IE недопустим, нужно указывать `setInterval(..., 1)`. - - - - - diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/setInterval-anim.view/index.html b/1-js/7-js-misc/3-setTimeout-setInterval/setInterval-anim.view/index.html deleted file mode 100755 index caa33217..00000000 --- a/1-js/7-js-misc/3-setTimeout-setInterval/setInterval-anim.view/index.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/setinterval-interval.png b/1-js/7-js-misc/3-setTimeout-setInterval/setinterval-interval.png deleted file mode 100644 index 0a815bdb..00000000 Binary files a/1-js/7-js-misc/3-setTimeout-setInterval/setinterval-interval.png and /dev/null differ diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/setinterval-interval@2x.png b/1-js/7-js-misc/3-setTimeout-setInterval/setinterval-interval@2x.png deleted file mode 100644 index 1e764160..00000000 Binary files a/1-js/7-js-misc/3-setTimeout-setInterval/setinterval-interval@2x.png and /dev/null differ diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/settimeout-interval.png b/1-js/7-js-misc/3-setTimeout-setInterval/settimeout-interval.png deleted file mode 100644 index b6909cc2..00000000 Binary files a/1-js/7-js-misc/3-setTimeout-setInterval/settimeout-interval.png and /dev/null differ diff --git a/1-js/7-js-misc/3-setTimeout-setInterval/settimeout-interval@2x.png b/1-js/7-js-misc/3-setTimeout-setInterval/settimeout-interval@2x.png deleted file mode 100644 index 657ec0db..00000000 Binary files a/1-js/7-js-misc/3-setTimeout-setInterval/settimeout-interval@2x.png and /dev/null differ diff --git a/1-js/7-js-misc/4-eval/1-eval-calculator/solution.md b/1-js/7-js-misc/4-eval/1-eval-calculator/solution.md deleted file mode 100644 index 0ad14328..00000000 --- a/1-js/7-js-misc/4-eval/1-eval-calculator/solution.md +++ /dev/null @@ -1,12 +0,0 @@ -Вычислить любое выражение нам поможет `eval`: - -```js -//+ demo run -var expr = prompt("Введите выражение?", '2*3+2'); - -alert( eval(expr) ); -``` - -При этом посетитель потенциально может делать все, что угодно. - -Чтобы ограничить выражения только математикой, вводимую строку нужно проверять при помощи [регулярных выражений](/regular-expressions-javascript) на наличие любых символов, кроме букв, пробелов и знаков пунктуации. diff --git a/1-js/7-js-misc/4-eval/1-eval-calculator/task.md b/1-js/7-js-misc/4-eval/1-eval-calculator/task.md deleted file mode 100644 index 771b005f..00000000 --- a/1-js/7-js-misc/4-eval/1-eval-calculator/task.md +++ /dev/null @@ -1,9 +0,0 @@ -# Eval-калькулятор - -[importance 4] - -Напишите интерфейс, который принимает математическое выражение (`prompt`) и возвращает его результат. - -Проверять выражение на корректность не требуется. - -[demo /] diff --git a/1-js/7-js-misc/4-eval/article.md b/1-js/7-js-misc/4-eval/article.md deleted file mode 100644 index 7f8ab9e2..00000000 --- a/1-js/7-js-misc/4-eval/article.md +++ /dev/null @@ -1,266 +0,0 @@ -# Запуск кода из строки: eval - -Функция `eval(code)` позволяет выполнить код, переданный ей в виде строки. - -Этот код будет выполнен в *текущей области видимости*. -[cut] - -## Использование eval - -В простейшем случае `eval` всего лишь выполняет код, например: - -```js -//+ run no-beautify -var a = 1; - -(function() { - - var a = 2; - -*!* - eval(' alert(a) '); // 2 -*/!* - -})() -``` - -Но он может не только выполнить код, но и вернуть результат. - -**Вызов `eval` возвращает последнее вычисленное выражение**: - -Например: - -```js -//+ run -alert( eval('1+1') ); // 2 -``` - -**При вызове `eval` имеет полный доступ к локальным переменным.** - -Это означает, что текущие переменные могут быть изменены или дополнены: - -```js -//+ untrusted refresh run -var x = 5; -eval(" alert( x ); x = 10"); // 5, доступ к старому значению -alert( x ); // 10, значение изменено внутри eval -``` - -[smart header="В строгом режиме `eval` имеет свою область видимости "] -В строгом режиме функционал `eval` чуть-чуть меняется. - -При `use strict` код внутри `eval` по-прежнему сможет читать и менять внешние переменные, однако переменные и функции, объявленные внутри `eval`, не попадут наружу. - -```js -//+ untrusted refresh run -"use strict"; - -*!* -eval("var a = 5; function f() { }"); -*/!* -alert( a ); // ошибка, переменная не определена -// функция f тоже не видна снаружи -``` - -Иными словами, в новом стандарте `eval` имеет свою область видимости, а к внешним переменным обращается через замыкание, аналогично тому, как работают обычные функции. -[/smart] - -## Неграмотное использование eval - -Начнём с того, что `eval` применяется очень редко. Действительно редко. Есть даже такое выражение "eval is evil" (eval -- зло). - -Причина проста: когда-то JavaScript был гораздо более слабым языком, чем сейчас, и некоторые вещи без `eval` было сделать невозможно. Но те времена давно прошли. И теперь найти тот случай, когда действительно надо выполнить код из строки -- это надо постараться. - -Но если вы действительно знаете, что это именно тот случай и вам необходим `eval` -- есть ряд вещей, которые нужно иметь в виду. - -Доступ к локальным переменным -- худшее, что можно сделать при `eval`. - -Дело в том, что локальные переменные могут быть легко переименованы: - -```js -function sayHi() { - var phrase = "Привет"; - eval(str); -} -``` - -Переменная `phrase` может быть переименована в `hello`, и если строка `str` обращается к ней -- будет ошибка. - -Современные средства сжатия JavaScript переименовывают локальные переменные автоматически. Это считается безопасным, так как локальная переменная видна лишь внутри функции и если в ней везде поменять `phrase` на `p`, то никто этого не заметит. - -До сжатия: - -```js -function sayHi() { - var phrase = "Привет"; - alert( phrase ); -} -``` - -После сжатия: - -```js -function sayHi() { - var a = "Привет"; - alert( a ); -} -``` - -На самом деле всё ещё проще -- в данном случае утилита сжатия автоматически уберёт переменную `a` и код станет таким: - -```js -function sayHi() { - alert( "Привет" ); -} -``` - -Итак, если где-то в функции есть `eval`, то его взаимодействие с локальными переменными будет нарушено с непредсказуемыми побочными эффектами. - -Некоторые инструменты сжатия предупреждают, когда видят `eval` или стараются вообще не сжимать такой код вместе с его внешними функциями, но всё это борьба с последствиями кривого кода. - -Как правило, `eval` не нужен, именно поэтому говорят, "eval is evil". - -## Запуск скрипта в глобальной области - -Ок, взаимодействовать с локальными переменными нельзя. - -Но допустим мы загрузили с сервера или вручную сгенерировали скрипт, который нужно выполнить. Желательно, в глобальной области, вне любых функций, чтобы он уж точно к локальным переменным отношения не имел. - -Здесь `eval` может пригодиться. Есть два трюка для выполнения кода в глобальной области: - -
            -
          1. Везде, кроме IE8-, достаточно вызвать `eval` не напрямую, а через `window.eval`. - -Вот так: - -```js -//+ run no-beautify -var a = 1; - -(function() { - - var a = 2; -*!* - window.eval(' alert(a) '); // 1, выполнено глобально везде, кроме IE8- -*/!* -})(); -``` - -
          2. -
          3. В IE8- можно применить нестандартную фунцию [execScript](http://msdn.microsoft.com/en-us/library/ie/ms536420%28v=vs.85%29.aspx). Она, как и `eval`, выполняет код, но всегда в глобальной области видимости и не возвращает значение.
          4. -
          - -Оба способа можно объединить в единой функции `globalEval(code)`, выполняющей код без доступа к локальным переменным: - -```js -//+ run no-beautify -*!* -function globalEval(code) { // объединим два способа в одну функцию - window.execScript ? execScript(code) : window.eval(code); -} -*/!* - -var a = 1; - -(function() { - - var a = 2; - - globalEval(' alert(a) '); // 1, во всех браузерах - -})(); -``` - -## Внешние данные через new Function - -Итак, у нас есть код, который, всё же, нужно выполнить динамически, через `eval`, но не просто скрипт -- а ему нужно передать какие-то значения. - -Как мы говорили ранее, считать их из локальных переменных нельзя: это подвержено ошибкам при переименовании переменных и сразу ломается при сжатии JavaScript. Да и вообще, неочевидно и криво. - -**К счастью, существует отличная альтернатива `eval`, которая позволяет корректно взаимодействовать c внешним кодом: `new Function`.** - -Вызов `new Function('a,b', '..тело..')` создает функцию с указанными аргументами `a,b` и телом. Как мы помним, доступа к текущему замыканию у такой функции не будет, но можно передать параметры и получить результат. - -Например: - -```js -//+ run -var a = 2, - b = 3; - -*!* -// вместо обращения к a,b через eval -// будем принимать их как аргументы динамически созданной функции -var mul = new Function('a, b', ' return a * b;'); -*/!* - -alert( mul(a, b) ); // 6 -``` - -## JSON и eval - -В браузерах IE7- не было методов `JSON.stringify` и `JSON.parse`, поэтому работа с JSON происходила через `eval`. - -Этот способ работы с JSON давно устарел, но его можно встретить кое-где в старом коде, так что для примера рассмотрим его. - -Вызов `eval(code)` выполняет код и, если это выражение, то возвращает его значение, поэтому можно в качестве кода передать JSON. - -Например: - -```js -//+ run -var str = '{ \ - "name": "Вася", \ - "age": 25 \ -}'; - -*!* -var user = eval('(' + str + ')'); -*/!* - -alert( user.name ); // Вася -``` - -Зачем здесь нужны скобки `eval( '(' + str + ')' )`, почему не просто `eval(str)`? - -...Всё дело в том, что в JavaScript с фигурной скобки `{` начинаются не только объекты, а в том числе и "блоки кода". Что имеется в виду в данном случае -- интерпретатор определяет по контексту. Если в основном потоке кода -- то блок, если в контексте выражения, то объект. - -Поэтому если передать в `eval` объект напрямую, то интерпретатор подумает, что это на самом деле блок кода, а там внутри какие-то двоеточия... - -Вот, для примера, `eval` без скобок, он выдаст ошибку: - -```js -//+ run -var user = eval('{ "name": "Вася", "age": 25 }'); -``` - -А если `eval` получает выражение в скобках `( ... )`, то интерпретатор точно знает, что это не блок кода, а объект: - -```js -//+ run -var user = eval('( { "name": "Вася", "age": 25 } )'); -alert( user.age ); // 25 -``` - -[warn header="Осторожно, злой JSON!"] -Если мы получаем JSON из недоверенного источника, например с чужого сервера, то разбор через `eval` может быть опасен. - -Например, чужой сервер может быть взломан (за свой-то код мы отвечаем, а за чужой -- нет) и вместо JSON вставлен злонамеренный JavaScript-код. - -**Поэтому рекомендуется, всё же, использовать `JSON.parse`.** - -При разборе через `JSON.parse` некорректный JSON просто приведёт к ошибке, а вот при разборе через `eval` этот код реально выполнится, он может вывести что-то на странице, перенаправить посетителя куда-то и т.п. -[/warn] - - -## Итого - -
            -
          • Функция `eval(str)` выполняет код и возвращает последнее вычисленное выражение. В современном JavaScript она используется редко.
          • -
          • Вызов `eval` может читать и менять локальные переменные. Это -- зло, которого нужно избегать.
          • -
          • Для выполнения скрипта в глобальной области используются трюк с `window.eval/execScript`. При этом локальные переменные не будут затронуты, так что такое выполнение безопасно и иногда, в редких архитектурах, может быть полезным.
          • -
          • Если нужно выполняемый код всё же должен взаимодействовать с локальными переменными -- используйте `new Function`. Создавайте функцию из строки и передавайте переменные ей, это надёжно и безопасно.
          • -
          - -Ещё примеры использования `eval` вы найдёте далее, в главе [](/json). - diff --git a/1-js/7-js-misc/5-exception/1-finally-or-code-after/solution.md b/1-js/7-js-misc/5-exception/1-finally-or-code-after/solution.md deleted file mode 100644 index 5befa284..00000000 --- a/1-js/7-js-misc/5-exception/1-finally-or-code-after/solution.md +++ /dev/null @@ -1,43 +0,0 @@ -Разница в поведении станет очевидной, если рассмотреть код внутри функции. - -Поведение будет различным, если управление каким-то образом выпрыгнет из `try..catch`. - -Например, `finally` сработает после `return`: - -```js -function f() { - try { - ... -*!* - return result; -*/!* - } catch (e) { - ... - } finally { - очистить ресурсы - } -} -``` - -Или же управление может выпрыгнуть из-за `throw`: - -```js -function f() { - try { - ... - - } catch (e) { - ... - if(не умею обрабатывать эту ошибку) { -*!* - throw e; -*/!* - } - - } finally { - очистить ресурсы - } -} -``` - -В этих случаях именно `finally` гарантирует выполнение кода до окончания работы `f`, просто код не будет вызван. \ No newline at end of file diff --git a/1-js/7-js-misc/5-exception/1-finally-or-code-after/task.md b/1-js/7-js-misc/5-exception/1-finally-or-code-after/task.md deleted file mode 100644 index 5c93daf5..00000000 --- a/1-js/7-js-misc/5-exception/1-finally-or-code-after/task.md +++ /dev/null @@ -1,43 +0,0 @@ -# Finally или просто код? - -[importance 5] - -Сравните два фрагмента кода. - -
            -
          1. Первый использует `finally` для выполнения кода по выходу из `try..catch`: - -```js -try { - начать работу - работать -} catch (e) { - обработать ошибку -} finally { -*!* - финализация: завершить работу -*/!* -} -``` - -
          2. -
          3. Второй фрагмент просто ставит очистку ресурсов за `try..catch`: - -```js -try { - начать работу -} catch (e) { - обработать ошибку -} - -*!* -финализация: завершить работу -*/!* -``` - -
          4. -
          - -Нужно, чтобы код финализации всегда выполнялся при выходе из блока `try..catch` и, таким образом, заканчивал начатую работу. Имеет ли здесь `finally` какое-то преимущество или оба фрагмента работают одинаково? - -Если имеет, то дайте пример когда код с `finally` работает верно, а без -- неверно. diff --git a/1-js/7-js-misc/5-exception/2-eval-calculator-errors/solution.md b/1-js/7-js-misc/5-exception/2-eval-calculator-errors/solution.md deleted file mode 100644 index a9cd06aa..00000000 --- a/1-js/7-js-misc/5-exception/2-eval-calculator-errors/solution.md +++ /dev/null @@ -1,36 +0,0 @@ -Вычислить любое выражение нам поможет `eval`: - -```js -//+ run -alert( eval("2+2") ); // 4 -``` - -Считываем выражение в цикле `while(true)`. Если при вычислении возникает ошибка -- ловим её в `try..catch`. - -Ошибкой считается, в том числе, получение `NaN` из `eval`, хотя при этом исключение не возникает. Можно бросить своё исключение в этом случае. - -Код решения: - -```js -//+ run demo -var expr, res; - -while (true) { - expr = prompt("Введите выражение?", '2-'); - if (expr == null) break; - - try { - res = eval(expr); - if (isNaN(res)) { - throw new Error("Результат неопределён"); - } - - break; - } catch (e) { - alert( "Ошибка: " + e.message + ", повторите ввод" ); - } -} - -alert( res ); -``` - diff --git a/1-js/7-js-misc/5-exception/2-eval-calculator-errors/task.md b/1-js/7-js-misc/5-exception/2-eval-calculator-errors/task.md deleted file mode 100644 index 73e5108f..00000000 --- a/1-js/7-js-misc/5-exception/2-eval-calculator-errors/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Eval-калькулятор с ошибками - -[importance 5] - -Напишите интерфейс, который принимает математическое выражение (в `prompt`) и результат его вычисления через `eval`. - -**При ошибке нужно выводить сообщение и просить переввести выражение**. - -Ошибкой считается не только некорректное выражение, такое как `2+`, но и выражение, возвращающее `NaN`, например `0/0`. - -[demo /] - - diff --git a/1-js/7-js-misc/5-exception/article.md b/1-js/7-js-misc/5-exception/article.md deleted file mode 100644 index 42b30917..00000000 --- a/1-js/7-js-misc/5-exception/article.md +++ /dev/null @@ -1,590 +0,0 @@ -# Перехват ошибок, "try..catch" - -Как бы мы хорошо ни программировали, в коде бывают ошибки. Или, как их иначе называют, "исключительные ситуации" (исключения). - -Обычно скрипт при ошибке, как говорят, "падает", с выводом ошибки в консоль. - -Но бывают случаи, когда нам хотелось бы как-то контролировать ситуацию, чтобы скрипт не просто "упал", а сделал что-то разумное. - -Для этого в JavaScript есть замечательная конструкция `try..catch`. - -[cut] - -## Конструкция try..catch - -Конструкция `try..catch` состоит из двух основных блоков: `try`, и затем `catch`: - -```js -try { - - // код ... - -} catch (err) { - - // обработка ошибки - -} -``` - -Работает она так: -
            -
          1. Выполняется код внутри блока `try`.
          2. -
          3. Если в нём ошибок нет, то блок `catch(err)` игнорируется, то есть выполнение доходит до конца `try` и потом прыгает через `catch`.
          4. -
          5. Если в нём возникнет ошибка, то выполнение `try` на ней прерывается, и управление прыгает в начало блока `catch(err)`. - -При этом переменная `err` (можно выбрать и другое название) будет содержать объект ошибки с подробной информацией о произошедшем.
          6. -
          - -**Таким образом, при ошибке в `try` скрипт не "падает", и мы получаем возможность обработать ошибку внутри `catch`.** - -Посмотрим это на примерах. - -
            -
          • Пример без ошибок: при запуске сработают `alert` `(1)` и `(2)`: - -```js -//+ run -try { - - alert('Начало блока try'); // *!*(1) <--*/!* - - // .. код без ошибок - - alert('Конец блока try'); // *!*(2) <--*/!* - -} catch(e) { - - alert('Блок catch не получит управление, так как нет ошибок'); // (3) - -} - -alert("Потом код продолжит выполнение..."); -``` - -
          • -
          • Пример с ошибкой: при запуске сработают `(1)` и `(3)`: - -```js -//+ run -try { - - alert('Начало блока try'); // *!*(1) <--*/!* - -*!* - lalala; // ошибка, переменная не определена! -*/!* - - alert('Конец блока try'); // (2) - -} catch(e) { - - alert('Ошибка ' + e.name + ":" + e.message + "\n" + e.stack); // *!*(3) <--*/!* - -} - -alert("Потом код продолжит выполнение..."); -``` - -
          • -
          - -[warn header="`try..catch` подразумевает, что код синтаксически верен"] -Если грубо нарушена структура кода, например не закрыта фигурная скобка или где-то стоит лишняя запятая, то никакой `try..catch` здесь не поможет. Такие ошибки называются *синтаксическими*, интерпретатор не может понять такой код. - -Здесь же мы рассматриваем ошибки *семантические*, то есть происходящие в корректном коде, в процессе выполнения. -[/warn] - - -[warn header="`try..catch` работает только в синхронном коде"] -Ошибку, которая произойдёт в коде, запланированном "на будущее", например, в `setTimeout`, `try..catch` не поймает: - -```js -//+ run -try { - setTimeout(function() { - throw new Error(); // вылетит в консоль - }, 1000); -} catch (e) { - alert( "не сработает" ); -} -``` - -На момент запуска функции, назначенной через `setTimeout`, этот код уже завершится, интерпретатор выйдет из блока `try..catch`. - -Чтобы поймать ошибку внутри функции из `setTimeout`, и `try..catch` должен быть в той же функции. -[/warn] - - -## Объект ошибки - -В примере выше мы видим объект ошибки. У него есть три основных свойства: -
          -
          `name`
          -
          Тип ошибки. Например, при обращении к несуществующей переменной: `"ReferenceError"`.
          -
          `message`
          -
          Текстовое сообщение о деталях ошибки.
          -
          `stack`
          -
          Везде, кроме IE8-, есть также свойство `stack`, которое содержит строку с информацией о последовательности вызовов, которая привела к ошибке.
          -
          - -В зависимости от браузера, у него могут быть и дополнительные свойства, см. Error в MDN и Error в MSDN. - -## Пример использования - -В JavaScript есть встроенный метод [JSON.parse(str)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse), который используется для чтения JavaScript-объектов (и не только) из строки. - -Обычно он используется для того, чтобы обрабатывать данные, полученные по сети, с сервера или из другого источника. - -Мы получаем их и вызываем метод `JSON.parse`, вот так: - -```js -//+ run -var data = '{"name":"Вася", "age": 30}'; // строка с данными, полученная с сервера - -var user = JSON.parse(data); // преобразовали строку в объект - -// теперь user -- это JS-объект с данными из строки -alert( user.name ); // Вася -alert( user.age ); // 30 -``` - -Более детально формат JSON разобран в главе [](/json). - -**В случае, если данные некорректны, `JSON.parse` генерирует ошибку, то есть скрипт "упадёт".** - -Устроит ли нас такое поведение? Конечно нет! - -Получается, что если вдруг что-то не так с данными, то посетитель никогда (если, конечно, не откроет консоль) об этом не узнает. - -А люди очень-очень не любят, когда что-то "просто падает", без всякого объявления об ошибке. - -**Бывают ситуации, когда без `try..catch` не обойтись, это -- одна из таких.** - -Используем `try..catch`, чтобы обработать некорректный ответ: - -```js -//+ run -var data = "Has Error"; // в данных ошибка - -try { - - var user = JSON.parse(data); // <-- ошибка при выполнении - alert( user.name ); // не сработает - -} catch (e) { - // ...выполнится catch - alert( "Извините, в данных ошибка, мы попробуем получить их ещё раз" ); - alert( e.name ); - alert( e.message ); -} -``` - -Здесь в `alert` только выводится сообщение, но область применения гораздо шире: можно повторять запрос, можно предлагать посетителю использовать альтернативный способ, можно отсылать информацию об ошибке на сервер... Свобода действий. - -## Генерация своих ошибок - -Представим на минуту, что данные являются корректным JSON... Но в этом объекте нет нужного свойства `name`: - -```js -//+ run -var data = '{ "age": 30 }'; // данные неполны - -try { - - var user = JSON.parse(data); // <-- выполнится без ошибок -*!* - alert( user.name ); // undefined -*/!* - -} catch (e) { - // не выполнится - alert( "Извините, в данных ошибка" ); -} -``` - -Вызов `JSON.parse` выполнится без ошибок, но ошибка в данных есть. И, так как свойство `name` обязательно должно быть, то для нас это такие же некорректные данные как и `"Has Error"`. - -Для того, чтобы унифицировать и объединить обработку ошибок парсинга и ошибок в структуре, мы воспользуемся оператором `throw`. - -### Оператор throw - -Оператор `throw` генерирует ошибку. - -Синтаксис: `throw <объект ошибки>`. - -Технически, в качестве объекта ошибки можно передать что угодно, это может быть даже не объект, а число или строка, но всё же лучше, чтобы это был объект, желательно -- совместимый со стандартным, то есть чтобы у него были как минимум свойства `name` и `message`. - -**В качестве конструктора ошибок можно использовать встроенный конструктор: `new Error(message)` или любой другой.** - -В JavaScript встроен ряд конструкторов для стандартных ошибок: `SyntaxError`, `ReferenceError`, `RangeError` и некоторые другие. Можно использовать и их, но только чтобы не было путаницы. - -В данном случае мы используем конструктор `new SyntaxError(message)`. Он создаёт ошибку того же типа, что и `JSON.parse`. - -```js -//+ run -var data = '{ "age": 30 }'; // данные неполны - -try { - - var user = JSON.parse(data); // <-- выполнится без ошибок - -*!* - if (!user.name) { - throw new SyntaxError("Данные некорректны"); - } -*/!* - - alert( user.name ); - -} catch (e) { - alert( "Извините, в данных ошибка" ); -} -``` - -Получилось, что блок `catch` -- единое место для обработки ошибок во всех случаях: когда ошибка выявляется при `JSON.parse` или позже. - -## Проброс исключения - -В коде выше мы предусмотрели обработку ошибок, которые возникают при некорректных данных. Но может ли быть так, что возникнет какая-то другая ошибка? - -Конечно, может! Код -- это вообще мешок с ошибками, бывает даже так что библиотеку выкладывают в открытый доступ, она там 10 лет лежит, её смотрят миллионы людей и на 11й год находятся опаснейшие ошибки. Такова жизнь, таковы люди. - -Блок `catch` в нашем примере предназначен для обработки ошибок, возникающих при некорректных данных. Если же в него попала какая-то другая ошибка, то вывод сообщения о "некорректных данных" будет дезинформацией посетителя. - -**Ошибку, о которой `catch` не знает, он не должен обрабатывать.** - -Такая техника называется *"проброс исключения"*: в `catch(e)` мы анализируем объект ошибки, и если он нам не подходит, то делаем `throw e`. - -При этом ошибка "выпадает" из `try..catch` наружу. Далее она может быть поймана либо внешним блоком `try..catch` (если есть), либо "повалит" скрипт. - -В примере ниже `catch` обрабатывает только ошибки `SyntaxError`, а остальные -- выбрасывает дальше: - -```js -//+ run -var data = '{ "name": "Вася", "age": 30 }'; // данные корректны - -try { - - var user = JSON.parse(data); - - if (!user.name) { - throw new SyntaxError("Ошибка в данных"); - } - -*!* - blabla(); // произошла непредусмотренная ошибка -*/!* - - alert( user.name ); - -} catch (e) { - -*!* - if (e.name == "SyntaxError") { - alert( "Извините, в данных ошибка" ); - } else { - throw e; - } -*/!* - -} -``` - -Заметим, что ошибка, которая возникла внутри блока `catch`, "выпадает" наружу, как если бы была в обычном коде. - -В следующем примере такие ошибки обрабатываются ещё одним, "более внешним" `try..catch`: - -```js -//+ run -function readData() { - var data = '{ "name": "Вася", "age": 30 }'; - - try { - // ... -*!* - blabla(); // ошибка! -*/!* - } catch (e) { - // ... -*!* - if (e.name != 'SyntaxError') { - throw e; // пробрасываем - } -*/!* - } -} - - -try { - readData(); -} catch (e) { -*!* - alert( "Поймал во внешнем catch: " + e ); // ловим -*/!* -} -``` - -В примере выше `try..catch` внутри `readData` умеет обрабатывать только `SyntaxError`, а внешний -- все ошибки. - -Без внешнего проброшенная ошибка "вывалилась" бы в консоль, с остановкой скрипта. - -## Оборачивание исключений - -И, для полноты картины -- последняя, самая продвинутая техника по работе с ошибками. Она, впрочем, является стандартной практикой во многих объектно-ориентированных языках. - -Цель функции `readData` в примере выше -- прочитать данные. При чтении могут возникать разные ошибки, не только `SyntaxError`, но и, возможно, к примеру, `URIError` (неправильное применение функций работы с URI), да и другие. - -Код, который вызвал `readData`, хотел бы иметь либо результат, либо информацию об ошибке. - -При этом очень важным является вопрос: обязан ли этот внешний код знать о всевозможных типах ошибок, которые могут возникать при чтении данных, и уметь перехватывать их? - -Обычно внешний код хотел бы работать "на уровень выше", и получать либо результат, либо "ошибку чтения данных", при этом какая именно ошибка произошла -- ему неважно. Ну, или, если будет важно, то хотелось бы иметь возможность это узнать, но обычно не требуется. - -Это важнейший общий подход к проектированию -- каждый участок функционала должен получать информацию на том уровне, который ему необходим. - -Мы его видим везде в грамотно построенном коде, но не всегда отдаём себе в этом отчёт. - -В данном случае, если при чтении данных происходит ошибка, то мы будем генерировать её в виде объекта `ReadError`, с соответствующим сообщением. А "исходную" ошибку -- на всякий случай тоже сохраним, присвоим в свойство `cause` (англ. -- причина). - -Выглядит это так: -```js -//+ run -function ReadError(message, cause) { - this.message = message; - this.cause = cause; - this.name = 'ReadError'; - this.stack = cause.stack; -} - -function readData() { - var data = '{ bad data }'; - - try { - // ... - JSON.parse(data); - // ... - } catch (e) { - // ... - if (e.name == 'URIError') { - throw new ReadError("Ошибка в URI", e); - } else if (e.name == 'SyntaxError') { -*!* - throw new ReadError("Синтаксическая ошибка в данных", e); -*/!* - } else { - throw e; // пробрасываем - } - } -} - - -try { - readData(); -} catch (e) { - if (e.name == 'ReadError') { - alert( e.message ); - alert( e.cause ); // оригинальная ошибка-причина - } else { - throw e; - } -} -``` - -Этот подход называют "оборачиванием" исключения, поскольку мы берём ошибки "более низкого уровня" и "заворачиваем" их в `ReadError`, которая соответствует текущей задаче. - -## Секция finally - -Конструкция `try..catch` может содержать ещё один блок: `finally`. - -Выглядит этот расширенный синтаксис так: - -```js -*!*try*/!* { - .. пробуем выполнить код .. -} *!*catch*/!*(e) { - .. перехватываем исключение .. -} *!*finally*/!* { - .. выполняем всегда .. -} -``` - -Секция `finally` не обязательна, но если она есть, то она выполняется всегда: -
            -
          • после блока `try`, если ошибок не было,
          • -
          • после `catch`, если они были.
          • -
          - -Попробуйте запустить такой код? - -```js -//+ run -try { - alert( 'try' ); - if (confirm('Сгенерировать ошибку?')) BAD_CODE(); -} catch (e) { - alert( 'catch' ); -} finally { - alert( 'finally' ); -} -``` - -У него два варианта работы: -
            -
          1. Если вы ответите на вопрос "Сгенерировать ошибку?" утвердительно, то `try -> catch -> finally`.
          2. -
          3. Если ответите отрицательно, то `try -> finally`. -
          - -**Секцию `finally` используют, чтобы завершить начатые операции при любом варианте развития событий.** - -Например, мы хотим подсчитать время на выполнение функции `sum(n)`, которая должна возвратить сумму чисел от `1` до `n` и работает рекурсивно: - -```js -//+ run -function sum(n) { - return n ? (n + sum(n - 1)) : 0; -} - -var n = +prompt('Введите n?', 100); - -var start = new Date(); - -try { - var result = sum(n); -} catch (e) { - result = 0; -*!* -} finally { - var diff = new Date() - start; -} -*/!* - -alert( result ? result : 'была ошибка' ); -alert( "Выполнение заняло " + diff ); -``` - -Здесь секция `finally` гарантирует, что время будет подсчитано в любых ситуациях -- при ошибке в `sum` или без неё. - -Вы можете проверить это, запустив код с указанием `n=100` -- будет без ошибки, `finally` выполнится после `try`, а затем с `n=100000` -- будет ошибка из-за слишком глубокой рекурсии, управление прыгнет в `finally` после `catch`. - -[smart header="`finally` и `return`"] - -Блок `finally` срабатывает при *любом* выходе из `try..catch`, в том числе и `return`. - -В примере ниже, из `try` происходит `return`, но `finally` получает управление до того, как контроль возвращается во внешний код. - -```js -//+ run -function func() { - - try { - // сразу вернуть значение - return 1; - - } catch (e) { - /* ... */ - } finally { -*!* - alert( 'finally' ); -*/!* - } -} - -alert( func() ); // сначала finally, потом 1 -``` - -Если внутри `try` были начаты какие-то процессы, которые нужно завершить по окончании работы, во в `finally` это обязательно будет сделано. - -Кстати, для таких случаев иногда используют `try..finally` вообще без `catch`: - -```js -//+ run -function func() { - try { - return 1; - } finally { - alert( 'Вызов завершён' ); - } -} - -alert( func() ); // сначала finally, потом 1 -``` - -В примере выше `try..finally` вообще не обрабатывает ошибки. Задача в другом -- выполнить код при любом выходе из `try` -- с ошибкой ли, без ошибок или через `return`. -[/smart] - - -## Последняя надежда: window.onerror - -Допустим, ошибка произошла вне блока `try..catch` или выпала из `try..catch` наружу, во внешний код. Скрипт упал. - -Можно ли как-то узнать о том, что произошло? Да, конечно. - -В браузере существует специальное свойство `window.onerror`, если в него записать функцию, то она выполнится и получит в аргументах сообщение ошибки, текущий URL и номер строки, откуда "выпала" ошибка. - -Необходимо лишь позаботиться, чтобы функция была назначена заранее. - -Например: - -```html - - -``` - -Как правило, роль `window.onerror` заключается в том, чтобы не оживить скрипт -- скорее всего, это уже невозможно, а в том, чтобы отослать сообщение об ошибке на сервер, где разработчики о ней узнают. - -Существуют даже специальные веб-сервисы, которые предоставляют скрипты для отлова и аналитики таких ошибок, например: [](https://errorception.com/) или [](http://www.muscula.com/). - - -## Итого - -Обработка ошибок -- большая и важная тема. - -В JavaScript для этого предусмотрены: - -
            -
          • Конструкция `try..catch..finally` -- она позволяет обработать произвольные ошибки в блоке кода. - -Это удобно в тех случаях, когда проще сделать действие и потом разбираться с результатом, чем долго и нудно проверять, не упадёт ли чего. - -Кроме того, иногда проверить просто невозможно, например `JSON.parse(str)` не позволяет "проверить" формат строки перед разбором. В этом случае блок `try..catch` необходим. - -Полный вид конструкции: - -```js -*!*try*/!* { - .. пробуем выполнить код .. -} *!*catch*/!*(e) { - .. перехватываем исключение .. -} *!*finally*/!* { - .. выполняем всегда .. -} -``` - -Возможны также варианты `try..catch` или `try..finally`.
          • -
          • Оператор `throw err` генерирует свою ошибку, в качестве `err` рекомендуется использовать объекты, совместимые с встроенным типом [Error](http://javascript.ru/Error), содержащие свойства `message` и `name`.
          • -
          - -Кроме того, мы рассмотрели некоторые важные приёмы: - -
            -
          • Проброс исключения -- `catch(err)` должен обрабатывать только те ошибки, которые мы рассчитываем в нём увидеть, остальные -- пробрасывать дальше через `throw err`. - -Определить, нужная ли это ошибка, можно, например, по свойству `name`.
          • -
          • Оборачивание исключений -- функция, в процессе работы которой возможны различные виды ошибок, может "обернуть их" в одну общую ошибку, специфичную для её задачи, и уже её пробросить дальше. Чтобы, при необходимости, можно было подробно определить, что произошло, исходную ошибку обычно присваивают в свойство этой, общей. Обычно это нужно для логирования.
          • -
          • В `window.onerror` можно присвоить функцию, которая выполнится при любой "выпавшей" из скрипта ошибке. Как правило, это используют в информационных целях, например отправляют информацию об ошибке на специальный сервис.
          • -
          - - diff --git a/1-js/7-js-misc/index.md b/1-js/7-js-misc/index.md deleted file mode 100644 index 091a171f..00000000 --- a/1-js/7-js-misc/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Некоторые другие возможности - -Различные возможности JavaScript, которые достаточно важны, но не заслужили отдельного раздела. \ No newline at end of file diff --git a/1-js/8-oop/1-about-oop/article.md b/1-js/8-oop/1-about-oop/article.md deleted file mode 100644 index 9039a50c..00000000 --- a/1-js/8-oop/1-about-oop/article.md +++ /dev/null @@ -1,56 +0,0 @@ -# Введение - -На протяжении долгого времени в программировании применялся [процедурный подход](http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D1%86%D0%B5%D0%B4%D1%83%D1%80%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5). При этом программа состоит из функций, вызывающих друг друга. - -Гораздо позже появилось [объектно-ориентированное программирование](http://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) (ООП), которое позволяет группировать функции и данные в единой сущности -- "объекте". - - -При объектно-ориентированной разработке мы описываем происходящее на уровне объектов, которые создаются, меняют свои свойства, взаимодействуют друг с другом и (в случае браузера) со страницей, в общем, живут. - -Например, "пользователь", "меню", "компонент интерфейса"... При объектно-ориентированном подходе каждый объект должен представлять собой интуитивно понятную сущность, у которой есть методы и данные. - -[warn header="ООП -- это не просто объекты"] -В JavaScript объекты часто используются просто как коллекции. - -Например, встроенный объект [Math](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Math) содержит функции (`Math.sin`, `Math.pow`, ...) и данные (константа `Math.PI`). - -При таком использовании объектов мы не можем сказать, что "применён объектно-ориентированный подход". В частности, никакую "единую сущность" `Math` из себя не представляет, это просто коллекция независимых функций с общим префиксом `Math`. -[/warn] - - -Мы уже работали в ООП-стиле, создавая объекты такого вида: - -```js -//+ run -function User(name) { - - this.sayHi = function() { - alert( "Привет, я " + name ); - }; - -} - -var vasya = new User("Вася"); // создали пользователя -vasya.sayHi(); // пользователь умеет говорить "Привет" -``` - -Здесь мы видим ярко выраженную сущность -- `User` (посетитель). Используя терминологию ООП, такие конструкторы часто называют *классами*, то есть можно сказать "класс `User`". - -[smart header="Класс в ООП"] -[Классом]("https://en.wikipedia.org/wiki/Class_(computer_programming)") в объектно-ориентированной разработке называют шаблон/программный код, предназначенный для создания объектов и методов. - -В JavaScript классы можно организовать по-разному. Говорят, что класс `User` написан в "функциональном" стиле. Далее мы также увидим "прототипный" стиль. -[/smart] - -ООП -- это наука о том, как делать правильную архитектуру. У неё есть свои принципы, например [SOLID](https://ru.wikipedia.org/wiki/SOLID_%28%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%29). - -По приёмам объектно-ориентированной разработки пишут книги, к примеру: - - - -Здесь мы не имеем возможности углубиться в теорию ООП, поэтому чтение таких книг рекомендуется. Хотя основные принципы, как использовать ООП правильно, мы, всё же, затронем. \ No newline at end of file diff --git a/1-js/8-oop/2-internal-external-interface/1-add-method-property-coffeemachine/solution.md b/1-js/8-oop/2-internal-external-interface/1-add-method-property-coffeemachine/solution.md deleted file mode 100644 index 5c92acb6..00000000 --- a/1-js/8-oop/2-internal-external-interface/1-add-method-property-coffeemachine/solution.md +++ /dev/null @@ -1,42 +0,0 @@ -Кофеварка с новым методом: - -```js -//+ run -function CoffeeMachine(power) { - this.waterAmount = 0; - - var WATER_HEAT_CAPACITY = 4200; -*!* - var timerId; -*/!* - var self = this; - - function getBoilTime() { - return self.waterAmount * WATER_HEAT_CAPACITY * 80 / power; - } - - function onReady() { - alert( 'Кофе готово!' ); - } - - this.run = function() { -*!* - timerId = setTimeout(onReady, getBoilTime()); -*/!* - }; - -*!* - this.stop = function() { - clearTimeout(timerId) - }; -*/!* -} - - -var coffeeMachine = new CoffeeMachine(50000); -coffeeMachine.waterAmount = 200; - -coffeeMachine.run(); -coffeeMachine.stop(); // кофе приготовлен не будет -``` - diff --git a/1-js/8-oop/2-internal-external-interface/1-add-method-property-coffeemachine/task.md b/1-js/8-oop/2-internal-external-interface/1-add-method-property-coffeemachine/task.md deleted file mode 100644 index 62177288..00000000 --- a/1-js/8-oop/2-internal-external-interface/1-add-method-property-coffeemachine/task.md +++ /dev/null @@ -1,43 +0,0 @@ -# Добавить метод и свойство кофеварке - -[importance 5] - -Улучшите готовый код кофеварки, который дан ниже: добавьте в кофеварку *публичный* метод `stop()`, который будет останавливать кипячение (через `clearTimeout`). - -```js -//+ run -function CoffeeMachine(power) { - this.waterAmount = 0; - - var WATER_HEAT_CAPACITY = 4200; - - var self = this; - - function getBoilTime() { - return self.waterAmount * WATER_HEAT_CAPACITY * 80 / power; - } - - function onReady() { - alert( 'Кофе готово!' ); - } - - this.run = function() { - setTimeout(onReady, getBoilTime()); - }; - -} -``` - -Вот такой код должен ничего не выводить: - -```js -var coffeeMachine = new CoffeeMachine(50000); -coffeeMachine.waterAmount = 200; - -coffeeMachine.run(); -coffeeMachine.stop(); // кофе приготовлен не будет -``` - -P.S. Текущую температуру воды вычислять и хранить не требуется. - -P.P.S. При решении вам, скорее всего, понадобится добавить *приватное* свойство `timerId`, которое будет хранить текущий таймер. \ No newline at end of file diff --git a/1-js/8-oop/2-internal-external-interface/article.md b/1-js/8-oop/2-internal-external-interface/article.md deleted file mode 100644 index b4058737..00000000 --- a/1-js/8-oop/2-internal-external-interface/article.md +++ /dev/null @@ -1,338 +0,0 @@ -# Внутренний и внешний интерфейс - -Один из важнейших принципов ООП -- отделение внутреннего интерфейса от внешнего. - -Это -- обязательная практика в разработке чего угодно сложнее hello world. - -Чтобы это понять, отвлечемся от разработки и переведем взгляд на объекты реального мира. - -Как правило, устройства, с которыми мы имеем дело, весьма сложны. Но *разделение интерфейса на внешний и внутренний* позволяет использовать их без малейших проблем. -[cut] -## Пример из жизни - -Например, кофеварка. Простая снаружи: кнопка, индикатор, отверстия,... И, конечно, результат -- кофе :) - - - -Но внутри... (картинка из пособия по ремонту) - - - -Масса деталей. Но мы можем пользоваться ей, совершенно не зная об этом. - -Кофеварки -- довольно-таки надежны, не правда ли? Можно пользоваться годами, и только когда что-то пойдет не так -- придется нести к мастеру. - -Секрет надежности и простоты кофеварки -- в том, что все детали отлажены и *спрятаны* внутри. - -Если снять с кофеварки защитный кожух, то использование её будет более сложным (куда нажимать?) и опасным (током ударить может). - -Как мы увидим, объекты очень схожи с кофеварками. - -Только для того, чтобы прятать внутренние детали, используется не кожух, а специальные средства языка и соглашения. - -## Внутренний и внешний интерфейс - -В программировании мы будем разделять методы и свойства объекта на две группы: - -
            -
          • *Внутренний интерфейс* -- это свойства и методы, доступ к которым может быть осуществлен только из других методов объекта, их также называют "приватными" (есть и другие термины, встретим их далее).
          • - -
          • *Внешний интерфейс* -- это свойства и методы, доступные снаружи объекта, их называют "публичными".
          • -
          - -Если продолжить аналогию с кофеваркой -- то, что спрятано внутри кофеварки: трубка кипятильника, нагревательный элемент, тепловой предохранитель и так далее -- это её внутренний интерфейс. - -Внутренний интерфейс используется для обеспечения работоспособности объекта, его детали используют друг друга. Например, трубка кипятильника подключена к нагревательному элементу. - -Но снаружи кофеварка закрыта специальным кожухом, чтобы никто к ним не подобрался. Детали скрыты и недоступны. Виден лишь внешний интерфейс. - -Получив объект, всё, что нужно для пользования им -- это знать внешний интерфейс. О внутреннем же знать вообще не обязательно. - -Это были общие слова по теории программирования. - -Далее мы реализуем кофеварку на JavaScript с приватными и публичными свойствами. В кофеварке много деталей, мы конечно, не будем моделировать каждый винтик, а сосредоточимся на основных приёмах разработки. - -## Шаг 1: публичное и приватное свойство - -Конструктор кофеварок будет называться `CoffeeMachine`. - -```js -//+ run -function CoffeeMachine(power) { - this.waterAmount = 0; // количество воды в кофеварке - - alert( 'Создана кофеварка мощностью: ' + power + ' ватт' ); -} - -// создать кофеварку -var coffeeMachine = new CoffeeMachine(100); - -// залить воды -coffeeMachine.waterAmount = 200; -``` - -**Локальные переменные, включая параметры конструктора, можно считать приватными свойствами.** - -В примере выше это `power` -- мощность кофеварки, которая указывается при создании и далее будет использована для расчёта времени кипячения. - -К локальным переменным конструктора нельзя обратиться снаружи, но они доступны внутри самого конструктора. - -**Свойства, записанные в `this`, можно считать публичными.** - -Здесь свойство `waterAmount` записано в объект, а значит -- доступно для модификации снаружи. Можно доливать и выливать воду в любом количестве. - -[smart header="Вопрос терминологии"] -Далее мы будем называть `power` как "локальной переменной", так и "приватным свойством" объекта. - -Это, смотря, с какой стороны посмотреть. - -Термины "приватное свойство/метод", "публичное свойство/метод" относятся к общей теории ООП. А их конкретная реализация в языке программирования может быть различной. - -Здесь ООП-принцип "приватного свойства" реализован через локальные переменные, поэтому и "локальная переменная" и "приватное свойство" -- правильные термины, в зависимости от того, с какой точки зрения взглянуть -- кода или архитектуры ООП. -[/smart] - - -## Шаг 2: публичный и приватный методы - -Добавим публичный метод `run`, запускающий кофеварку, а также вспомогательные внутренние методы `getBoilTime` и `onReady`: - -```js -//+ run -function CoffeeMachine(power) { - - this.waterAmount = 0; - -*!* - // расчёт времени для кипячения - function getBoilTime() { - return 1000; // точная формула расчета будет позже - } - - // что делать по окончании процесса - function onReady() { - alert( 'Кофе готово!' ); - } - - this.run = function() { - // setTimeout - встроенная функция, - // она запустит onReady через getBoilTime() миллисекунд - setTimeout(onReady, getBoilTime()); - }; -*/!* -} - -var coffeeMachine = new CoffeeMachine(100); -coffeeMachine.waterAmount = 200; - -coffeeMachine.run(); -``` - -Приватные методы, такие как `onReady`, `getBoilTime` могут быть объявлены как вложенные функции. - -В результате естественным образом получается, что доступ к ним (через замыкание) имеют только другие функции, объявленные в том же конструкторе. - -## Шаг 3: константа - -Для расчёта времени на кипячение воды используется формула `c*m*ΔT / power`, где: -
            -
          • `c` -- коэффициент теплоёмкости воды, физическая константа равная `4200`.
          • -
          • `m` -- масса воды, которую нужно согреть.
          • -
          • `ΔT` -- температура, на которую нужно подогреть, будем считать, что изначально вода -- комнатной температуры 20°С, то есть до 100° нужно греть на `ΔT=80`.
          • -
          • `power` -- мощность.
          • -
          - -Используем её в более реалистичном варианте `getBoilTime()`, включающем использование приватных свойств и константу: - -```js -//+ run -"use strict" - -function CoffeeMachine(power) { - - this.waterAmount = 0; - -*!* - // физическая константа - удельная теплоёмкость воды для getBoilTime - var WATER_HEAT_CAPACITY = 4200; - - // расчёт времени для кипячения - function getBoilTime() { - return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power; // ошибка! - } -*/!* - - // что делать по окончании процесса - function onReady() { - alert( 'Кофе готово!' ); - } - - this.run = function() { - setTimeout(onReady, getBoilTime()); - }; - -} - -var coffeeMachine = new CoffeeMachine(1000); -coffeeMachine.waterAmount = 200; - -coffeeMachine.run(); -``` - -Удельная теплоёмкость `WATER_HEAT_CAPACITY` выделена большими буквами, так как это константа. - -Внимание, при запуске кода выше в методе `getBoilTime` будет ошибка. Как вы думаете, почему? - -## Шаг 4: доступ к объекту из внутреннего метода - -Внутренний метод вызывается так: `getBoilTime()`. А чему при этом равен `this`?... Как вы наверняка помните, в современном стандарте он будет `undefined` (в старом -- `window`), из-за этого при чтении `this.waterAmount` возникнет ошибка! - -Её можно решить, если вызвать `getBoilTime` с явным указанием контекста: `getBoilTime.call(this)`: - -```js -//+ run -function CoffeeMachine(power) { - this.waterAmount = 0; - var WATER_HEAT_CAPACITY = 4200; - - function getBoilTime() { - return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power; - } - - function onReady() { - alert( 'Кофе готово!' ); - } - - this.run = function() { -*!* - setTimeout(onReady, getBoilTime.call(this)); -*/!* - }; - -} - -// создаю кофеварку, мощностью 100000W чтобы кипятила быстро -var coffeeMachine = new CoffeeMachine(100000); -coffeeMachine.waterAmount = 200; - -coffeeMachine.run(); -``` - -Такой подход будет работать, но он не очень-то удобен. Ведь получается, что теперь везде, где мы хотим вызвать `getBoilTime`, нужно явно указывать контекст, т.е. писать `getBoilTime.call(this)`. - -К счастью существуют более элегантные решения. - -### Привязка через bind - -Можно при объявлении привязать `getBoilTime` к объекту через `bind`, тогда вопрос контекста отпадёт сам собой: - -```js -//+ run -function CoffeeMachine(power) { - this.waterAmount = 0; - - var WATER_HEAT_CAPACITY = 4200; - -*!* - var getBoilTime = function() { - return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power; - }.bind(this); -*/!* - - function onReady() { - alert( 'Кофе готово!' ); - } - - this.run = function() { -*!* - setTimeout(onReady, getBoilTime()); -*/!* - }; - -} - -var coffeeMachine = new CoffeeMachine(100000); -coffeeMachine.waterAmount = 200; - -coffeeMachine.run(); -``` - -Это решение будет работать, теперь функцию можно просто вызывать без `call`. Но объявление функции стало менее красивым. - -### Сохранение this в замыкании - -Пожалуй, самый удобный и часто применяемый путь решения состоит в том, чтобы предварительно скопировать `this` во вспомогательную переменную и обращаться из внутренних функций уже к ней. - -Вот так: - -```js -//+ run -function CoffeeMachine(power) { - this.waterAmount = 0; - - var WATER_HEAT_CAPACITY = 4200; - -*!* - var self = this; - - function getBoilTime() { - return self.waterAmount * WATER_HEAT_CAPACITY * 80 / power; - } -*/!* - - function onReady() { - alert( 'Кофе готово!' ); - } - - this.run = function() { - setTimeout(onReady, getBoilTime()); - }; - -} - -var coffeeMachine = new CoffeeMachine(100000); -coffeeMachine.waterAmount = 200; - -coffeeMachine.run(); -``` - -Теперь `getBoilTime` получает `self` из замыкания. - -**Конечно, чтобы это работало, мы не должны изменять `self`, а все приватные методы, которые хотят иметь доступ к текущему объекту, должны использовать внутри себя `self` вместо `this`.** - -Вместо `self` можно использовать любое другое имя переменной, например `var me = this`. - -## Итого - -Итак, мы сделали кофеварку с публичными и приватными методами и заставили их корректно работать. - -В терминологии ООП отделение и защита внутреннего интерфейса называется [инкапсуляция](http://ru.wikipedia.org/wiki/%D0%98%D0%BD%D0%BA%D0%B0%D0%BF%D1%81%D1%83%D0%BB%D1%8F%D1%86%D0%B8%D1%8F_%28%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%29). - -Кратко перечислим бонусы, которые она даёт: - -
          -
          Защита пользователей от выстрела себе в ногу
          -
          Представьте, команда разработчиков пользуется кофеваркой. Кофеварка создана фирмой "Лучшие Кофеварки" и, в общем, работает хорошо, но с неё сняли защитный кожух и, таким образом, внутренний интерфейс стал доступен. - -Все разработчики цивилизованны -- и пользуются кофеваркой как обычно. Но хитрый Вася решил, что он самый умный, и подкрутил кое-что внутри кофеварки, чтобы кофе заваривался покрепче. Вася не знал, что те изменения, которые он произвёл, приведут к тому, что кофеварка испортится через два дня. - -Виноват, разумеется, не только Вася, но и тот, кто снял защитный кожух с кофеварки, и тем самым позволил Васе проводить манипуляции. - -В программировании -- то же самое. Если пользователь объекта будет менять то, что не рассчитано на изменение снаружи -- последствия могут быть непредсказуемыми. -
          -
          Удобство в поддержке
          -
          Ситуация в программировании сложнее, чем с кофеваркой, т.к. кофеварку один раз купили и всё, а программа может улучшаться и дорабатываться. - -**При наличии чётко выделенного внешнего интерфейса, разработчик может свободно менять внутренние свойства и методы, без оглядки на коллег.** - -Гораздо легче разрабатывать, если знаешь, что ряд методов (все внутренние) можно переименовывать, менять их параметры, и вообще, переписать как угодно, так как внешний код к ним абсолютно точно не обращается. - -Ближайшая аналогия в реальной жизни -- это когда выходит "новая версия" кофеварки, которая работает гораздо лучше. Разработчик мог переделать всё внутри, но пользоваться ей по-прежнему просто, так как внешний интерфейс сохранён.
          -
          Управление сложностью
          -
          Люди обожают пользоваться вещами, которые просты с виду. А что внутри -- дело десятое. - -Программисты здесь не исключение. - -**Всегда удобно, когда детали реализации скрыты, и доступен простой, понятно документированный внешний интерфейс.** -
          -
          - diff --git a/1-js/8-oop/2-internal-external-interface/coffee-inside.jpg b/1-js/8-oop/2-internal-external-interface/coffee-inside.jpg deleted file mode 100755 index 60f84664..00000000 Binary files a/1-js/8-oop/2-internal-external-interface/coffee-inside.jpg and /dev/null differ diff --git a/1-js/8-oop/2-internal-external-interface/coffee.jpg b/1-js/8-oop/2-internal-external-interface/coffee.jpg deleted file mode 100755 index ee26e1c0..00000000 Binary files a/1-js/8-oop/2-internal-external-interface/coffee.jpg and /dev/null differ diff --git a/1-js/8-oop/3-getters-setters/1-object-with-getters-setters/solution.md b/1-js/8-oop/3-getters-setters/1-object-with-getters-setters/solution.md deleted file mode 100644 index bb0bb14d..00000000 --- a/1-js/8-oop/3-getters-setters/1-object-with-getters-setters/solution.md +++ /dev/null @@ -1,29 +0,0 @@ -Решение: - -```js -//+ run -function User() { - - var firstName, surname; - - this.setFirstName = function(newFirstName) { - firstName = newFirstName; - }; - - this.setSurname = function(newSurname) { - surname = newSurname; - }; - - this.getFullName = function() { - return firstName + ' ' + surname; - } -} - -var user = new User(); -user.setFirstName("Петя"); -user.setSurname("Иванов"); - -alert( user.getFullName() ); // Петя Иванов -``` - -Обратим внимание, что для "геттера" `getFullName` нет соответствующего свойства объекта, он конструирует ответ "на лету". Это нормально. Одна из целей существования геттеров/сеттеров -- как раз и есть изоляция внутренних свойств объекта, чтобы можно было их как угодно менять, генерировать "на лету", а внешний интерфейс оставался тем же. \ No newline at end of file diff --git a/1-js/8-oop/3-getters-setters/1-object-with-getters-setters/task.md b/1-js/8-oop/3-getters-setters/1-object-with-getters-setters/task.md deleted file mode 100644 index d9efe056..00000000 --- a/1-js/8-oop/3-getters-setters/1-object-with-getters-setters/task.md +++ /dev/null @@ -1,25 +0,0 @@ -# Написать объект с геттерами и сеттерами - -[importance 4] - -Напишите конструктор `User` для создания объектов: -
            -
          • С приватными свойствами имя `firstName` и фамилия `surname`.
          • -
          • С сеттерами для этих свойств.
          • -
          • С геттером `getFullName()`, который возвращает полное имя.
          • -
          - -Должен работать так: - -```js -function User() { - /* ваш код */ -} - -var user = new User(); -user.setFirstName("Петя"); -user.setSurname("Иванов"); - -alert( user.getFullName() ); // Петя Иванов -``` - diff --git a/1-js/8-oop/3-getters-setters/2-getter-power/solution.md b/1-js/8-oop/3-getters-setters/2-getter-power/solution.md deleted file mode 100644 index b82c109a..00000000 --- a/1-js/8-oop/3-getters-setters/2-getter-power/solution.md +++ /dev/null @@ -1,28 +0,0 @@ - - -```js -function CoffeeMachine(power, capacity) { - //... - this.setWaterAmount = function(amount) { - if (amount < 0) { - throw new Error("Значение должно быть положительным"); - } - if (amount > capacity) { - throw new Error("Нельзя залить воды больше, чем " + capacity); - } - - waterAmount = amount; - }; - - this.getWaterAmount = function() { - return waterAmount; - }; - -*!* - this.getPower = function() { - return power; - }; -*/!* -} -``` - diff --git a/1-js/8-oop/3-getters-setters/2-getter-power/task.md b/1-js/8-oop/3-getters-setters/2-getter-power/task.md deleted file mode 100644 index 7301c6f5..00000000 --- a/1-js/8-oop/3-getters-setters/2-getter-power/task.md +++ /dev/null @@ -1,32 +0,0 @@ -# Добавить геттер для power - -[importance 5] - -Добавьте кофеварке геттер для приватного свойства `power`, чтобы внешний код мог узнать мощность кофеварки. - -Исходный код: - -```js -function CoffeeMachine(power, capacity) { - //... - this.setWaterAmount = function(amount) { - if (amount < 0) { - throw new Error("Значение должно быть положительным"); - } - if (amount > capacity) { - throw new Error("Нельзя залить воды больше, чем " + capacity); - } - - waterAmount = amount; - }; - - this.getWaterAmount = function() { - return waterAmount; - }; - -} -``` - -Обратим внимание, что ситуация, когда у свойства `power` есть геттер, но нет сеттера -- вполне обычна. - -Здесь это означает, что мощность `power` можно указать лишь при создании кофеварки и в дальнейшем её можно прочитать, но нельзя изменить. \ No newline at end of file diff --git a/1-js/8-oop/3-getters-setters/3-add-public-coffeemachine/solution.md b/1-js/8-oop/3-getters-setters/3-add-public-coffeemachine/solution.md deleted file mode 100644 index 41aa318a..00000000 --- a/1-js/8-oop/3-getters-setters/3-add-public-coffeemachine/solution.md +++ /dev/null @@ -1,47 +0,0 @@ -В решении ниже `addWater` будет просто вызывать `setWaterAmount`. - -```js -//+ run -function CoffeeMachine(power, capacity) { - var waterAmount = 0; - - var WATER_HEAT_CAPACITY = 4200; - - function getTimeToBoil() { - return waterAmount * WATER_HEAT_CAPACITY * 80 / power; - } - - this.setWaterAmount = function(amount) { - if (amount < 0) { - throw new Error("Значение должно быть положительным"); - } - if (amount > capacity) { - throw new Error("Нельзя залить больше, чем " + capacity); - } - - waterAmount = amount; - }; - -*!* - this.addWater = function(amount) { - this.setWaterAmount(waterAmount + amount); - }; -*/!* - - function onReady() { - alert( 'Кофе готов!' ); - } - - this.run = function() { - setTimeout(onReady, getTimeToBoil()); - }; - -} - -var coffeeMachine = new CoffeeMachine(100000, 400); -coffeeMachine.addWater(200); -coffeeMachine.addWater(100); -coffeeMachine.addWater(300); // Нельзя залить больше.. -coffeeMachine.run(); -``` - diff --git a/1-js/8-oop/3-getters-setters/3-add-public-coffeemachine/task.md b/1-js/8-oop/3-getters-setters/3-add-public-coffeemachine/task.md deleted file mode 100644 index e123fed4..00000000 --- a/1-js/8-oop/3-getters-setters/3-add-public-coffeemachine/task.md +++ /dev/null @@ -1,52 +0,0 @@ -# Добавить публичный метод кофеварке - -[importance 5] - -Добавьте кофеварке публичный метод `addWater(amount)`, который будет добавлять воду. - -При этом, конечно же, должны происходить все необходимые проверки -- на положительность и превышение ёмкости. - -Исходный код: - -```js -function CoffeeMachine(power, capacity) { - var waterAmount = 0; - - var WATER_HEAT_CAPACITY = 4200; - - function getTimeToBoil() { - return waterAmount * WATER_HEAT_CAPACITY * 80 / power; - } - - this.setWaterAmount = function(amount) { - if (amount < 0) { - throw new Error("Значение должно быть положительным"); - } - if (amount > capacity) { - throw new Error("Нельзя залить больше, чем " + capacity); - } - - waterAmount = amount; - }; - - function onReady() { - alert( 'Кофе готов!' ); - } - - this.run = function() { - setTimeout(onReady, getTimeToBoil()); - }; - -} -``` - -Вот такой код должен приводить к ошибке: - -```js -var coffeeMachine = new CoffeeMachine(100000, 400); -coffeeMachine.addWater(200); -coffeeMachine.addWater(100); -coffeeMachine.addWater(300); // Нельзя залить больше, чем 400 -coffeeMachine.run(); -``` - diff --git a/1-js/8-oop/3-getters-setters/4-setter-onready/solution.md b/1-js/8-oop/3-getters-setters/4-setter-onready/solution.md deleted file mode 100644 index 2aca1b6f..00000000 --- a/1-js/8-oop/3-getters-setters/4-setter-onready/solution.md +++ /dev/null @@ -1,69 +0,0 @@ - - -```js -//+ run -function CoffeeMachine(power, capacity) { - var waterAmount = 0; - - var WATER_HEAT_CAPACITY = 4200; - - function getTimeToBoil() { - return waterAmount * WATER_HEAT_CAPACITY * 80 / power; - } - - this.setWaterAmount = function(amount) { - // ... проверки пропущены для краткости - waterAmount = amount; - }; - - this.getWaterAmount = function(amount) { - return waterAmount; - }; - - function onReady() { - alert( 'Кофе готов!' ); - } - -*!* - this.setOnReady = function(newOnReady) { - onReady = newOnReady; - }; -*/!* - - this.run = function() { -*!* - setTimeout(function() { - onReady(); - }, getTimeToBoil()); -*/!* - }; - -} - -var coffeeMachine = new CoffeeMachine(20000, 500); -coffeeMachine.setWaterAmount(150); - -coffeeMachine.run(); - -*!* -coffeeMachine.setOnReady(function() { - var amount = coffeeMachine.getWaterAmount(); - alert( 'Готов кофе: ' + amount + 'мл' ); // Готов кофе: 150 мл -}); -*/!* -``` - -Обратите внимание на два момента в решении: -
            -
          1. В сеттере `setOnReady` параметр называется `newOnReady`. Мы не можем назвать его `onReady`, так как тогда изнутри сеттера мы никак не доберёмся до внешнего (старого значения): - -```js -// нерабочий вариант -this.setOnReady = function(onReady) { - onReady = onReady; // ??? внешняя переменная onReady недоступна -}; -``` - -
          2. -
          3. Чтобы `setOnReady` можно было вызывать в любое время, в `setTimeout` передаётся не `onReady`, а анонимная функция `function() { onReady() }`, которая возьмёт текущий (установленный последним) `onReady` из замыкания.
          4. -
          \ No newline at end of file diff --git a/1-js/8-oop/3-getters-setters/4-setter-onready/task.md b/1-js/8-oop/3-getters-setters/4-setter-onready/task.md deleted file mode 100644 index 2e960697..00000000 --- a/1-js/8-oop/3-getters-setters/4-setter-onready/task.md +++ /dev/null @@ -1,59 +0,0 @@ -# Создать сеттер для onReady - -[importance 5] - -Обычно когда кофе готов, мы хотим что-то сделать, например выпить его. - -Сейчас при готовности срабатывает функция `onReady`, но она жёстко задана в коде: - -```js -function CoffeeMachine(power, capacity) { - var waterAmount = 0; - - var WATER_HEAT_CAPACITY = 4200; - - function getTimeToBoil() { - return waterAmount * WATER_HEAT_CAPACITY * 80 / power; - } - - this.setWaterAmount = function(amount) { - // ... проверки пропущены для краткости - waterAmount = amount; - }; - - this.getWaterAmount = function(amount) { - return waterAmount; - }; - -*!* - function onReady() { - alert( 'Кофе готов!' ); - } -*/!* - - this.run = function() { - setTimeout(onReady, getTimeToBoil()); - }; - -} -``` - -Создайте сеттер `setOnReady`, чтобы код снаружи мог назначить свой `onReady`, вот так: - -```js -var coffeeMachine = new CoffeeMachine(20000, 500); -coffeeMachine.setWaterAmount(150); - -*!* -coffeeMachine.setOnReady(function() { - var amount = coffeeMachine.getWaterAmount(); - alert( 'Готов кофе: ' + amount + 'мл' ); // Кофе готов: 150 мл -}); -*/!* - -coffeeMachine.run(); -``` - -P.S. Значение `onReady` по умолчанию должно быть таким же, как и раньше. - -P.P.S. Постарайтесь сделать так, чтобы `setOnReady` можно было вызвать не только до, но и *после* запуска кофеварки, то есть чтобы функцию `onReady` можно было изменить в любой момент до её срабатывания. \ No newline at end of file diff --git a/1-js/8-oop/3-getters-setters/5-coffeemachine-add-isrunning/solution.md b/1-js/8-oop/3-getters-setters/5-coffeemachine-add-isrunning/solution.md deleted file mode 100644 index 8f61ff53..00000000 --- a/1-js/8-oop/3-getters-setters/5-coffeemachine-add-isrunning/solution.md +++ /dev/null @@ -1,62 +0,0 @@ -Код решения модифицирует функцию `run` и добавляет приватный идентификатор таймера `timerId`, по наличию которого мы судим о состоянии кофеварки: - -```js -//+ run -function CoffeeMachine(power, capacity) { - var waterAmount = 0; - -*!* - var timerId; - - this.isRunning = function() { - return !!timerId; - }; -*/!* - - var WATER_HEAT_CAPACITY = 4200; - - function getTimeToBoil() { - return waterAmount * WATER_HEAT_CAPACITY * 80 / power; - } - - this.setWaterAmount = function(amount) { - // ... проверки пропущены для краткости - waterAmount = amount; - }; - - this.getWaterAmount = function(amount) { - return waterAmount; - }; - - function onReady() { - alert( 'Кофе готов!' ); - } - - this.setOnReady = function(newOnReady) { - onReady = newOnReady; - }; - - this.run = function() { -*!* - timerId = setTimeout(function() { - timerId = null; - onReady(); - }, getTimeToBoil()); - }; -*/!* - -} - -var coffeeMachine = new CoffeeMachine(20000, 500); -coffeeMachine.setWaterAmount(100); - -alert( 'До: ' + coffeeMachine.isRunning() ); // До: false - -coffeeMachine.run(); -alert( 'В процессе: ' + coffeeMachine.isRunning() ); // В процессе: true - -coffeeMachine.setOnReady(function() { - alert( "После: " + coffeeMachine.isRunning() ); // После: false -}); -``` - diff --git a/1-js/8-oop/3-getters-setters/5-coffeemachine-add-isrunning/task.md b/1-js/8-oop/3-getters-setters/5-coffeemachine-add-isrunning/task.md deleted file mode 100644 index 7d8ff96c..00000000 --- a/1-js/8-oop/3-getters-setters/5-coffeemachine-add-isrunning/task.md +++ /dev/null @@ -1,25 +0,0 @@ -# Добавить метод isRunning - -[importance 5] - -Из внешнего кода мы хотели бы иметь возможность понять -- запущена кофеварка или нет. - -Для этого добавьте кофеварке публичный метод `isRunning()`, который будет возвращать `true`, если она запущена и `false`, если нет. - -Нужно, чтобы такой код работал: - -```js -var coffeeMachine = new CoffeeMachine(20000, 500); -coffeeMachine.setWaterAmount(100); - -alert( 'До: ' + coffeeMachine.isRunning() ); // До: false - -coffeeMachine.run(); -alert( 'В процессе: ' + coffeeMachine.isRunning() ); // В процессе: true - -coffeeMachine.setOnReady(function() { - alert( "После: " + coffeeMachine.isRunning() ); // После: false -}); -``` - -Исходный код возьмите из решения [предыдущей задачи](/task/setter-onready). \ No newline at end of file diff --git a/1-js/8-oop/3-getters-setters/article.md b/1-js/8-oop/3-getters-setters/article.md deleted file mode 100644 index e97a6e1b..00000000 --- a/1-js/8-oop/3-getters-setters/article.md +++ /dev/null @@ -1,167 +0,0 @@ -# Геттеры и сеттеры - -Для *управляемого* доступа к состоянию объекта используют специальные функции, так называемые "геттеры" и "сеттеры". -[cut] - -## Геттер и сеттер для воды - -На текущий момент количество воды в кофеварке является публичным свойством `waterAmount`: - -```js -//+ run -function CoffeeMachine(power) { - // количество воды в кофеварке - this.waterAmount = 0; - - ... -} -``` - -Это немного опасно. Ведь в это свойство можно записать произвольное количество воды, хоть весь мировой океан. - -```js -// не помещается в кофеварку! -coffeeMachine.waterAmount = 1000000; -``` - -Это ещё ничего, гораздо хуже, что можно наоборот -- вылить больше, чем есть: - -```js -// и не волнует, было ли там столько воды вообще! -coffeeMachine.waterAmount -= 1000000; -``` - -Так происходит потому, что свойство полностью доступно снаружи. - -Чтобы не было таких казусов, нам нужно ограничить контроль над свойством со стороны внешнего кода. - -**Для лучшего контроля над свойством его делают приватным, а запись значения осуществляется через специальный метод, который называют *"сеттер"* (setter method).** - -Типичное название для сеттера -- `setСвойство`, например, в случае с кофеваркой таким сеттером будет метод `setWaterAmount`: - -```js -//+ run -function CoffeeMachine(power, capacity) { // capacity - ёмкость кофеварки - var waterAmount = 0; - - var WATER_HEAT_CAPACITY = 4200; - - function getTimeToBoil() { - return waterAmount * WATER_HEAT_CAPACITY * 80 / power; - } - -*!* - // "умная" установка свойства - this.setWaterAmount = function(amount) { - if (amount < 0) { - throw new Error("Значение должно быть положительным"); - } - if (amount > capacity) { - throw new Error("Нельзя залить воды больше, чем " + capacity); - } - - waterAmount = amount; - }; -*/!* - - function onReady() { - alert( 'Кофе готов!' ); - } - - this.run = function() { - setTimeout(onReady, getTimeToBoil()); - }; - -} - -var coffeeMachine = new CoffeeMachine(1000, 500); -coffeeMachine.setWaterAmount(600); // упс, ошибка! -``` - -Теперь `waterAmount` -- внутреннее свойство, его можно записать (через сеттер), но, увы, нельзя прочитать. - -**Для того, чтобы дать возможность внешнему коду узнать его значение, создадим специальную функцию -- "геттер" (getter method).** - -Геттеры обычно имеют название вида `getСвойство`, в данном случае `getWaterAmount`: - -```js -//+ run -function CoffeeMachine(power, capacity) { - //... - this.setWaterAmount = function(amount) { - if (amount < 0) { - throw new Error("Значение должно быть положительным"); - } - if (amount > capacity) { - throw new Error("Нельзя залить воды больше, чем " + capacity); - } - - waterAmount = amount; - }; - -*!* - this.getWaterAmount = function() { - return waterAmount; - }; -*/!* -} - -var coffeeMachine = new CoffeeMachine(1000, 500); -coffeeMachine.setWaterAmount(450); -alert( coffeeMachine.getWaterAmount() ); // 450 -``` - -## Единый геттер-сеттер - -Для большего удобства иногда делают единый метод, который называется так же, как свойство и отвечает *и за запись и за чтение*. - -При вызове без параметров такой метод возвращает свойство, а при передаче параметра -- назначает его. - -Выглядит это так: - -```js -//+ run -function CoffeeMachine(power, capacity) { - var waterAmount = 0; - -*!* - this.waterAmount = function(amount) { -*/!* - // вызов без параметра, значит режим геттера, возвращаем свойство - if (!arguments.length) return waterAmount; - - // иначе режим сеттера - if (amount < 0) { - throw new Error("Значение должно быть положительным"); - } - if (amount > capacity) { - throw new Error("Нельзя залить воды больше, чем " + capacity); - } - - waterAmount = amount; - }; - -} - -var coffeeMachine = new CoffeeMachine(1000, 500); - -// пример использования -*!* -coffeeMachine.waterAmount(450); -alert( coffeeMachine.waterAmount() ); // 450 -*/!* -``` - -Единый геттер-сеттер используется реже, чем две отдельные функции, но в некоторых JavaScript-библиотеках, например [jQuery](http://jquery.com) и [D3](http://d3js.org) подобный подход принят на уровне концепта. - -## Итого - -
            -
          • Для большего контроля над присвоением и чтением значения, вместо свойства делают "функцию-геттер" и "функцию-сеттер", геттер возвращает значение, сеттер -- устанавливает.
          • -
          • Если свойство предназначено только для чтения, то может быть только геттер, только для записи -- только сеттер.
          • -
          • В качестве альтернативы паре геттер/сеттер применяют единую функцию, которая без аргументов ведёт себя как геттер, а с аргументом -- как сеттер.
          • -
          - -Также можно организовать геттеры/сеттеры для свойства, не меняя структуры кода, через [дескрипторы свойств](/descriptors-getters-setters). - - diff --git a/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/solution.md b/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/solution.md deleted file mode 100644 index 9f35452d..00000000 --- a/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/solution.md +++ /dev/null @@ -1,15 +0,0 @@ -Изменения в методе `run`: - -```js -this.run = function() { -*!* - if (!this._enabled) { - throw new Error("Кофеварка выключена"); - } -*/!* - - setTimeout(onReady, 1000); -}; -``` - - diff --git a/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/solution.view/index.html b/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/solution.view/index.html deleted file mode 100755 index 053c8198..00000000 --- a/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/solution.view/index.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/source.view/index.html b/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/source.view/index.html deleted file mode 100755 index 6d287989..00000000 --- a/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/source.view/index.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/task.md b/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/task.md deleted file mode 100644 index d90fce6a..00000000 --- a/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/task.md +++ /dev/null @@ -1,21 +0,0 @@ -# Запускать только при включённой кофеварке - -[importance 5] - -В коде `CoffeeMachine` сделайте так, чтобы метод `run` выводил ошибку, если кофеварка выключена. - -В итоге должен работать такой код: - -```js -var coffeeMachine = new CoffeeMachine(10000); -coffeeMachine.run(); // ошибка, кофеварка выключена! -``` - -А вот так -- всё в порядке: - -```js -var coffeeMachine = new CoffeeMachine(10000); -coffeeMachine.enable(); -coffeeMachine.run(); // ...Кофе готов! -``` - diff --git a/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/solution.view/index.html b/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/solution.view/index.html deleted file mode 100755 index 61ce8414..00000000 --- a/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/solution.view/index.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/task.md b/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/task.md deleted file mode 100644 index 298cd301..00000000 --- a/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/task.md +++ /dev/null @@ -1,16 +0,0 @@ -# Останавливать кофеварку при выключении - -[importance 5] - -Когда кофеварку выключают -- текущая варка кофе должна останавливаться. - -Например, следующий код кофе не сварит: - -```js -var coffeeMachine = new CoffeeMachine(10000); -coffeeMachine.enable(); -coffeeMachine.run(); -coffeeMachine.disable(); // остановит работу, ничего не выведет -``` - -Реализуйте это на основе решения [предыдущей задачи](/task/coffeemachine-fix-run). diff --git a/1-js/8-oop/5-functional-inheritance/3-inherit-fridge/solution.md b/1-js/8-oop/5-functional-inheritance/3-inherit-fridge/solution.md deleted file mode 100644 index 38957f28..00000000 --- a/1-js/8-oop/5-functional-inheritance/3-inherit-fridge/solution.md +++ /dev/null @@ -1,29 +0,0 @@ -Решение: - -```js -function Fridge(power) { - // унаследовать - Machine.apply(this, arguments); - - var food = []; // приватное свойство food - - this.addFood = function() { - if (!this._enabled) { - throw new Error("Холодильник выключен"); - } - if (food.length + arguments.length >= this._power / 100) { - throw new Error("Нельзя добавить, не хватает мощности"); - } - for (var i = 0; i < arguments.length; i++) { - food.push(arguments[i]); // добавить всё из arguments - } - }; - - this.getFood = function() { - // копируем еду в новый массив, чтобы манипуляции с ним не меняли food - return food.slice(); - }; - -} -``` - diff --git a/1-js/8-oop/5-functional-inheritance/3-inherit-fridge/task.md b/1-js/8-oop/5-functional-inheritance/3-inherit-fridge/task.md deleted file mode 100644 index 150bdb0f..00000000 --- a/1-js/8-oop/5-functional-inheritance/3-inherit-fridge/task.md +++ /dev/null @@ -1,67 +0,0 @@ -# Унаследуйте холодильник - -[importance 4] - -Создайте класс для холодильника `Fridge(power)`, наследующий от `Machine`, с приватным свойством `food` и методами `addFood(...)`, `getFood()`: -
            -
          • Приватное свойство `food` хранит массив еды.
          • -
          • Публичный метод `addFood(item)` добавляет в массив `food` новую еду, доступен вызов с несколькими аргументами `addFood(item1, item2...)` для добавления нескольких элементов сразу.
          • -
          • Если холодильник выключен, то добавить еду нельзя, будет ошибка.
          • -
          • Максимальное количество еды ограничено `power/100`, где `power` -- мощность холодильника, указывается в конструкторе. При попытке добавить больше -- будет ошибка
          • -
          • Публичный метод `getFood()` возвращает еду в виде массива, добавление или удаление элементов из которого не должно влиять на свойство `food` холодильника.
          • -
          - -Код для проверки: - -```js -var fridge = new Fridge(200); -fridge.addFood("котлета"); // ошибка, холодильник выключен -``` - -Ещё код для проверки: - -```js -// создать холодильник мощностью 500 (не более 5 еды) -var fridge = new Fridge(500); -fridge.enable(); -fridge.addFood("котлета"); -fridge.addFood("сок", "зелень"); -fridge.addFood("варенье", "пирог", "торт"); // ошибка, слишком много еды -``` - -Код использования холодильника без ошибок: - -```js -var fridge = new Fridge(500); -fridge.enable(); -fridge.addFood("котлета"); -fridge.addFood("сок", "варенье"); - -var fridgeFood = fridge.getFood(); -alert( fridgeFood ); // котлета, сок, варенье - -// добавление элементов не влияет на еду в холодильнике -fridgeFood.push("вилка", "ложка"); - -alert( fridge.getFood() ); // внутри по-прежнему: котлета, сок, варенье -``` - -Исходный код класса `Machine`, от которого нужно наследовать: - -```js -function Machine(power) { - this._power = power; - this._enabled = false; - - var self = this; - - this.enable = function() { - self._enabled = true; - }; - - this.disable = function() { - self._enabled = false; - }; -} -``` - diff --git a/1-js/8-oop/5-functional-inheritance/4-add-methods-fridge/solution.md b/1-js/8-oop/5-functional-inheritance/4-add-methods-fridge/solution.md deleted file mode 100644 index e7752658..00000000 --- a/1-js/8-oop/5-functional-inheritance/4-add-methods-fridge/solution.md +++ /dev/null @@ -1,89 +0,0 @@ - - -```js -//+ run -function Machine(power) { - this._power = power; - this._enabled = false; - - var self = this; - - this.enable = function() { - self._enabled = true; - }; - - this.disable = function() { - self._enabled = false; - }; -} - -function Fridge(power) { - // унаследовать - Machine.apply(this, arguments); - - var food = []; // приватное свойство food - - this.addFood = function() { - if (!this._enabled) { - throw new Error("Холодильник выключен"); - } - if (food.length + arguments.length >= this._power / 100) { - throw new Error("Нельзя добавить, не хватает мощности"); - } - for (var i = 0; i < arguments.length; i++) { - food.push(arguments[i]); // добавить всё из arguments - } - - }; - - this.getFood = function() { - // копируем еду в новый массив, чтобы манипуляции с ним не меняли food - return food.slice(); - }; - -*!* - this.filterFood = function(filter) { - return food.filter(filter); - }; - - this.removeFood = function(item) { - var idx = food.indexOf(item); - if (idx != -1) food.splice(idx, 1); - }; -*/!* -} - -var fridge = new Fridge(500); -fridge.enable(); -fridge.addFood({ - title: "котлета", - calories: 100 -}); -fridge.addFood({ - title: "сок", - calories: 30 -}); -fridge.addFood({ - title: "зелень", - calories: 10 -}); -fridge.addFood({ - title: "варенье", - calories: 150 -}); - -var dietItems = fridge.filterFood(function(item) { - return item.calories < 50; -}); - -fridge.removeFood("нет такой еды"); // без эффекта -alert( fridge.getFood().length ); // 4 - -dietItems.forEach(function(item) { - alert( item.title ); // сок, зелень - fridge.removeFood(item); -}); - -alert( fridge.getFood().length ); // 2 -``` - diff --git a/1-js/8-oop/5-functional-inheritance/4-add-methods-fridge/task.md b/1-js/8-oop/5-functional-inheritance/4-add-methods-fridge/task.md deleted file mode 100644 index 46a4db21..00000000 --- a/1-js/8-oop/5-functional-inheritance/4-add-methods-fridge/task.md +++ /dev/null @@ -1,48 +0,0 @@ -# Добавьте методы в холодильник - -[importance 5] - -Добавьте в холодильник методы: -
            -
          • Публичный метод `filterFood(func)`, который возвращает всю еду, для которой `func(item) == true`
          • -
          • Публичный метод `removeFood(item)`, который удаляет еду `item` из холодильника.
          • -
          - -Код для проверки: - -```js -var fridge = new Fridge(500); -fridge.enable(); -fridge.addFood({ - title: "котлета", - calories: 100 -}); -fridge.addFood({ - title: "сок", - calories: 30 -}); -fridge.addFood({ - title: "зелень", - calories: 10 -}); -fridge.addFood({ - title: "варенье", - calories: 150 -}); - -fridge.removeFood("нет такой еды"); // без эффекта -alert( fridge.getFood().length ); // 4 - -var dietItems = fridge.filterFood(function(item) { - return item.calories < 50; -}); - -dietItems.forEach(function(item) { - alert( item.title ); // сок, зелень - fridge.removeFood(item); -}); - -alert( fridge.getFood().length ); // 2 -``` - -В качестве исходного кода используйте решение [предыдущей задачи](/task/inherit-fridge). diff --git a/1-js/8-oop/5-functional-inheritance/5-override-disable/solution.md b/1-js/8-oop/5-functional-inheritance/5-override-disable/solution.md deleted file mode 100644 index ec5fbb18..00000000 --- a/1-js/8-oop/5-functional-inheritance/5-override-disable/solution.md +++ /dev/null @@ -1,68 +0,0 @@ - - -```js -//+ run -function Machine(power) { - this._power = power; - this._enabled = false; - - var self = this; - - this.enable = function() { - self._enabled = true; - }; - - this.disable = function() { - self._enabled = false; - }; -} - -function Fridge(power) { - Machine.apply(this, arguments); - - var food = []; // приватное свойство food - - this.addFood = function() { - if (!this._enabled) { - throw new Error("Холодильник выключен"); - } - if (food.length + arguments.length >= this._power / 100) { - throw new Error("Нельзя добавить, не хватает мощности"); - } - for (var i = 0; i < arguments.length; i++) { - food.push(arguments[i]); // добавить всё из arguments - } - - }; - - this.getFood = function() { - // копируем еду в новый массив, чтобы манипуляции с ним не меняли food - return food.slice(); - }; - - this.filterFood = function(filter) { - return food.filter(filter); - }; - - this.removeFood = function(item) { - var idx = food.indexOf(item); - if (idx != -1) food.splice(idx, 1); - }; - -*!* - var parentDisable = this.disable; - this.disable = function() { - if (food.length) { - throw new Error("Нельзя выключить: внутри еда"); - } - parentDisable(); - }; -*/!* -} - -var fridge = new Fridge(500); -fridge.enable(); -fridge.addFood("кус-кус"); -fridge.disable(); // ошибка, в холодильнике есть еда -``` - diff --git a/1-js/8-oop/5-functional-inheritance/5-override-disable/task.md b/1-js/8-oop/5-functional-inheritance/5-override-disable/task.md deleted file mode 100644 index 482ffaaa..00000000 --- a/1-js/8-oop/5-functional-inheritance/5-override-disable/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# Переопределите disable - -[importance 5] - -Переопределите метод `disable` холодильника, чтобы при наличии в нём еды он выдавал ошибку. - - -Код для проверки: - -```js -var fridge = new Fridge(500); -fridge.enable(); -fridge.addFood("кус-кус"); -fridge.disable(); // ошибка, в холодильнике есть еда -``` - -В качестве исходного кода используйте решение [предыдущей задачи](/task/add-methods-fridge). diff --git a/1-js/8-oop/5-functional-inheritance/article.md b/1-js/8-oop/5-functional-inheritance/article.md deleted file mode 100644 index 3afd5a93..00000000 --- a/1-js/8-oop/5-functional-inheritance/article.md +++ /dev/null @@ -1,403 +0,0 @@ -# Функциональное наследование - -Наследование -- это создание новых "классов" на основе существующих. - -В JavaScript его можно реализовать несколькими путями, один из которых -- с использованием наложения конструкторов, мы рассмотрим в этой главе. -[cut] - -## Зачем наследование? - -Ранее мы обсуждали различные реализации кофеварки. Продолжим эту тему далее. - -Хватит ли нам только кофеварки для удобной жизни? Вряд ли... Скорее всего, ещё понадобятся как минимум холодильник, микроволновка, а возможно и другие *машины*. - -В реальной жизни у этих *машин* есть базовые правила пользования. Например, большая кнопка -- включение, шнур с розеткой нужно воткнуть в питание и т.п. - -Можно сказать, что "у всех машин есть общие свойства, а конкретные машины могут их дополнять". - -Именно поэтому, увидев новую технику, мы уже можем что-то с ней сделать, даже не читая инструкцию. - -Механизм наследования позволяет определить базовый класс `Машина`, в нём описать то, что свойственно всем машинам, а затем на его основе построить другие, более конкретные: `Кофеварка`, `Холодильник` и т.п. - -[smart header="В веб-разработке всё так же"] -В веб-разработке нам могут понадобиться классы `Меню`, `Табы`, `Диалог` и другие компоненты интерфейса. В них всех обычно есть что-то общее. - -Можно выделить такой общий функционал в класс `Компонент` и наследовать их от него, чтобы не дублировать код. -[/smart] - -## Наследование от Machine - -Базовый класс "машина" `Machine` будет реализовывать общего вида методы "включить" `enable()` и "выключить" `disable()`: - -```js -function Machine() { - var enabled = false; - - this.enable = function() { - enabled = true; - }; - - this.disable = function() { - enabled = false; - }; -} -``` - -Унаследуем от него кофеварку. При этом она получит эти методы автоматически: - -```js -function CoffeeMachine(power) { -*!* - Machine.call(this); // отнаследовать -*/!* - - var waterAmount = 0; - - this.setWaterAmount = function(amount) { - waterAmount = amount; - }; - -} - -var coffeeMachine = new CoffeeMachine(10000); - -*!* -coffeeMachine.enable(); -coffeeMachine.setWaterAmount(100); -coffeeMachine.disable(); -*/!* -``` - -Наследование реализовано вызовом `Machine.call(this)` в начале конструктора `CoffeeMachine`. - -Он вызывает функцию `Machine`, передавая ей в качестве контекста `this` текущий объект. `Machine`, в процессе выполнения, записывает в `this` различные полезные свойства и методы, в нашем случае `this.enable` и `this.disable`. - -Далее конструктор `CoffeeMachine` продолжает выполнение и может добавить свои свойства и методы. - -В результате мы получаем объект `coffeeMachine`, который включает в себя методы из `Machine` и `CoffeeMachine`. - -## Защищённые свойства - -В коде выше есть одна проблема. - -**Наследник не имеет доступа к приватным свойствам родителя.** - -Иначе говоря, если кофеварка захочет обратиться к `enabled`, то её ждёт разочарование: - -```js -//+ run -function Machine() { - var enabled = false; - - this.enable = function() { - enabled = true; - }; - - this.disable = function() { - enabled = false; - }; -} - -function CoffeeMachine(power) { - Machine.call(this); - - this.enable(); - -*!* - // ошибка, переменная не определена! - alert( enabled ); -*/!* -} - -var coffeeMachine = new CoffeeMachine(10000); -``` - -Это естественно, ведь `enabled` -- локальная переменная функции `Machine`. Она находится в другой области видимости. - -**Чтобы наследник имел доступ к свойству, оно должно быть записано в `this`.** - -При этом, чтобы обозначить, что свойство является внутренним, его имя начинают с подчёркивания `_`. - -```js -//+ run -function Machine() { -*!* - this._enabled = false; // вместо var enabled -*/!* - - this.enable = function() { - this._enabled = true; - }; - - this.disable = function() { - this._enabled = false; - }; -} - -function CoffeeMachine(power) { - Machine.call(this); - - this.enable(); - -*!* - alert( this._enabled ); // true -*/!* -} - -var coffeeMachine = new CoffeeMachine(10000); -``` - -Подчёркивание в начале свойства -- общепринятый знак, что свойство является внутренним, предназначенным лишь для доступа из самого объекта и его наследников. Такие свойства называют *защищёнными*. - -Технически, залезть в него из внешнего кода, конечно, возможно, но приличный программист так делать не будет. - -## Перенос свойства в защищённые - -У `CoffeeMachine` есть приватное свойство `power`. Сейчас мы его тоже сделаем защищённым и перенесём в `Machine`, поскольку "мощность" свойственна всем машинам, а не только кофеварке. - -```js -//+ run -function Machine(power) { -*!* - this._power = power; // (1) -*/!* - - this._enabled = false; - - this.enable = function() { - this._enabled = true; - }; - - this.disable = function() { - this._enabled = false; - }; -} - -function CoffeeMachine(power) { -*!* - Machine.apply(this, arguments); // (2) -*/!* - - alert( this._enabled ); // false - alert( this._power ); // 10000 -} - -var coffeeMachine = new CoffeeMachine(10000); -``` - -Теперь все машины `Machine` имеют мощность `power`. Обратим внимание, что мы из параметра конструктора сразу скопировали её в объект в строке `(1)`. Иначе она была бы недоступна из наследников. - -В строке `(2)` мы теперь вызываем не просто `Machine.call(this)`, а расширенный вариант: `Machine.apply(this, arguments)`, который вызывает `Machine` в текущем контексте вместе с передачей текущих аргументов. - -Можно было бы использовать и более простой вызов `Machine.call(this, power)`, но использование `apply` гарантирует передачу всех аргументов, вдруг их количество увеличится -- не надо будет переписывать. - -## Переопределение методов - -Итак, мы получили класс `CoffeeMachine`, который наследует от `Machine`. - -Аналогичным образом мы можем унаследовать от `Machine` холодильник `Fridge`, микроволновку `MicroOven` и другие классы, которые разделяют общий "машинный" функционал, то есть имеют мощность и их можно включать/выключать. - -Для этого достаточно вызвать `Machine` текущем контексте, а затем добавить свои методы. - -```js -// Fridge может добавить и свои аргументы, -// которые в Machine не будут использованы -function Fridge(power, temperature) { - Machine.apply(this, arguments); - - // ... -} -``` - -Бывает так, что реализация конкретного метода машины в наследнике имеет свои особенности. - -Можно, конечно, объявить в `CoffeeMachine` свой `enable`: - -```js -function CoffeeMachine(power, capacity) { - Machine.apply(this, arguments); - - // переопределить this.enable - this.enable = function() { - /* enable для кофеварки */ - }; -} -``` - -...Однако, как правило, мы хотим не заменить, а *расширить* метод родителя, добавить к нему что-то. Например, сделать так, чтобы при включении кофеварка тут же запускалась. - -Для этого метод родителя предварительно копируют в переменную, и затем вызывают внутри нового `enable` -- там, где считают нужным: - -```js -function CoffeeMachine(power) { - Machine.apply(this, arguments); - -*!* - var parentEnable = this.enable; // (1) - this.enable = function() { // (2) - parentEnable.call(this); // (3) - this.run(); // (4) - } -*/!* - - ... -} -``` - -**Общая схема переопределения метода (по строкам выделенного фрагмента кода):** - -
            -
          1. Копируем доставшийся от родителя метод `this.enable` в переменную, например `parentEnable`.
          2. -
          3. Заменяем `this.enable` на свою функцию...
          4. -
          5. ...Которая по-прежнему реализует старый функционал через вызов `parentEnable`.
          6. -
          7. ...И в дополнение к нему делает что-то своё, например запускает приготовление кофе.
          8. -
          - -Обратим внимание на строку `(3)`. - -В ней родительский метод вызывается так: `parentEnable.call(this)`. Если бы вызов был таким: `parentEnable()`, то ему бы не передался текущий `this` и возникла бы ошибка. - -Технически, можно сделать возможность вызывать его и как `parentEnable()`, но тогда надо гарантировать, что контекст будет правильным, например привязать его при помощи `bind` или при объявлении, в родителе, вообще не использовать `this`, а получать контекст через замыкание, вот так: - -```js -//+ run -function Machine(power) { - this._enabled = false; - -*!* - var self = this; -*/!* - - this.enable = function() { -*!* - // используем внешнюю переменную вместо this - self._enabled = true; -*/!* - }; - - this.disable = function() { - self._enabled = false; - }; - -} - -function CoffeeMachine(power) { - Machine.apply(this, arguments); - - var waterAmount = 0; - - this.setWaterAmount = function(amount) { - waterAmount = amount; - }; - -*!* - var parentEnable = this.enable; - this.enable = function() { - parentEnable(); // теперь можно вызывать как угодно, this не важен - this.run(); - } -*/!* - - function onReady() { - alert( 'Кофе готово!' ); - } - - this.run = function() { - setTimeout(onReady, 1000); - }; - -} - -var coffeeMachine = new CoffeeMachine(10000); -coffeeMachine.setWaterAmount(50); -coffeeMachine.enable(); -``` - -В коде выше родительский метод `parentEnable = this.enable` успешно продолжает работать даже при вызове без контекста. А всё потому, что использует `self` внутри. - -## Итого - -Организация наследования, которая описана в этой главе, называется "функциональным паттерном наследования". - -Её общая схема (кратко): - -
            -
          1. Объявляется конструктор родителя `Machine`. В нём могут быть приватные (private), публичные (public) и защищённые (protected) свойства: - -```js -function Machine(params) { - // локальные переменные и функции доступны только внутри Machine - var privateProperty; - - // публичные доступны снаружи - this.publicProperty = ...; - - // защищённые доступны внутри Machine и для потомков - // мы договариваемся не трогать их снаружи - this._protectedProperty = ... -} - -var machine = new Machine(...) -machine.public(); -``` - -
          2. -
          3. Для наследования конструктор потомка вызывает родителя в своём контексте через `apply`. После чего может добавить свои переменные и методы: - -```js -function CoffeeMachine(params) { - // универсальный вызов с передачей любых аргументов -*!* - Machine.apply(this, arguments); -*/!* - - this.coffeePublicProperty = ... -} - -var coffeeMachine = new CoffeeMachine(...); -coffeeMachine.publicProperty(); -coffeeMachine.coffeePublicProperty(); -``` - -
          4. -
          5. В `CoffeeMachine` свойства, полученные от родителя, можно перезаписать своими. Но обычно требуется не заменить, а расширить метод родителя. Для этого он предварительно копируется в переменную: - -```js -function CoffeeMachine(params) { - Machine.apply(this, arguments); - -*!* - var parentProtected = this._protectedProperty; - this._protectedProperty = function(args) { - parentProtected.apply(this, args); // (*) - // ... - }; -*/!* -} -``` - -Строку `(*)` можно упростить до `parentProtected(args)`, если метод родителя не использует `this`, а, например, привязан к `var self = this`: - -```js -function Machine(params) { - var self = this; - - this._protected = function() { - self.property = "value"; - }; -} -``` - -
          6. -
          - -Надо сказать, что способ наследования, описанный в этой главе, используется нечасто. - -В следующих главах мы будем изучать прототипный подход, который обладаем рядом преимуществ. - -Но знать и понимать его необходимо, поскольку во многих существующих библиотеках классы написаны в функциональном стиле, и расширять/наследовать от них можно только так. - - - - - diff --git a/1-js/8-oop/index.md b/1-js/8-oop/index.md deleted file mode 100644 index 292b2d77..00000000 --- a/1-js/8-oop/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# ООП в функциональном стиле - -Инкапсуляция и наследование в функциональном стиле, а также расширенные возможности объектов JavaScript. \ No newline at end of file diff --git a/1-js/9-prototypes/1-prototype/1-property-after-delete/solution.md b/1-js/9-prototypes/1-prototype/1-property-after-delete/solution.md deleted file mode 100644 index f6308c17..00000000 --- a/1-js/9-prototypes/1-prototype/1-property-after-delete/solution.md +++ /dev/null @@ -1,5 +0,0 @@ -
            -
          1. `true`, свойство взято из `rabbit`.
          2. -
          3. `null`, свойство взято из `animal`.
          4. -
          5. `undefined`, свойства больше нет.
          6. -
          \ No newline at end of file diff --git a/1-js/9-prototypes/1-prototype/1-property-after-delete/task.md b/1-js/9-prototypes/1-prototype/1-property-after-delete/task.md deleted file mode 100644 index 70b4e3ba..00000000 --- a/1-js/9-prototypes/1-prototype/1-property-after-delete/task.md +++ /dev/null @@ -1,28 +0,0 @@ -# Чему равно cвойство после delete? - -[importance 5] - -Какие значения будут выводиться в коде ниже? - -```js -var animal = { - jumps: null -}; -var rabbit = { - jumps: true -}; - -rabbit.__proto__ = animal; - -alert( rabbit.jumps ); // ? (1) - -delete rabbit.jumps; - -alert( rabbit.jumps ); // ? (2) - -delete animal.jumps; - -alert( rabbit.jumps ); // ? (3) -``` - -Итого три вопроса. \ No newline at end of file diff --git a/1-js/9-prototypes/1-prototype/2-proto-and-this/proto5.png b/1-js/9-prototypes/1-prototype/2-proto-and-this/proto5.png deleted file mode 100755 index 055a4447..00000000 Binary files a/1-js/9-prototypes/1-prototype/2-proto-and-this/proto5.png and /dev/null differ diff --git a/1-js/9-prototypes/1-prototype/2-proto-and-this/proto6.png b/1-js/9-prototypes/1-prototype/2-proto-and-this/proto6.png deleted file mode 100755 index d8a63a58..00000000 Binary files a/1-js/9-prototypes/1-prototype/2-proto-and-this/proto6.png and /dev/null differ diff --git a/1-js/9-prototypes/1-prototype/2-proto-and-this/solution.md b/1-js/9-prototypes/1-prototype/2-proto-and-this/solution.md deleted file mode 100644 index 4206298f..00000000 --- a/1-js/9-prototypes/1-prototype/2-proto-and-this/solution.md +++ /dev/null @@ -1,18 +0,0 @@ -**Ответ: свойство будет записано в `rabbit`.** - -Если коротко -- то потому что `this` будет указывать на `rabbit`, а прототип при записи не используется. - -Если в деталях -- посмотрим как выполняется `rabbit.eat()`: -
            -
          1. Интерпретатор ищет `rabbit.eat`, чтобы его вызвать. Но свойство `eat` отсутствует в объекте `rabbit`, поэтому он идет по ссылке `rabbit.__proto__` и находит это свойство там. - -
          2. -
          3. Функция `eat` запускается. Контекст ставится равным объекту перед точкой, т.е. `this = rabbit`. - -Итак -- получается, что команда `this.full = true` устанавливает свойство `full` в самом объекте `rabbit`. Итог: - - -
          4. -
          - -Эта задача демонстрирует, что несмотря на то, в каком прототипе находится свойство, это никак не влияет на установку `this`, которая осуществляется по своим, независимым правилам. \ No newline at end of file diff --git a/1-js/9-prototypes/1-prototype/2-proto-and-this/task.md b/1-js/9-prototypes/1-prototype/2-proto-and-this/task.md deleted file mode 100644 index 346de8e1..00000000 --- a/1-js/9-prototypes/1-prototype/2-proto-and-this/task.md +++ /dev/null @@ -1,24 +0,0 @@ -# Прототип и this - -[importance 5] - -Сработает ли вызов `rabbit.eat()` ? - -Если да, то в какой именно объект он запишет свойство `full`: в `rabbit` или `animal`? - -```js -var animal = { - eat: function() { - this.full = true; - } -}; - -var rabbit = { - __proto__: animal -}; - -*!* -rabbit.eat(); -*/!* -``` - diff --git a/1-js/9-prototypes/1-prototype/3-search-algorithm/solution.md b/1-js/9-prototypes/1-prototype/3-search-algorithm/solution.md deleted file mode 100644 index 60ba9501..00000000 --- a/1-js/9-prototypes/1-prototype/3-search-algorithm/solution.md +++ /dev/null @@ -1,33 +0,0 @@ -
            -
          1. Расставим `__proto__`: - -```js -//+ run -var head = { - glasses: 1 -}; - -var table = { - pen: 3 -}; -table.__proto__ = head; - -var bed = { - sheet: 1, - pillow: 2 -}; -bed.__proto__ = table; - -var pockets = { - money: 2000 -}; -pockets.__proto__ = bed; - -alert( pockets.pen ); // 3 -alert( bed.glasses ); // 1 -alert( table.money ); // undefined -``` - -
          2. -
          3. **В современных браузерах, с точки зрения производительности, нет разницы, брать свойство из объекта или прототипа.** Они запоминают, где было найдено свойство и в следующий раз при запросе, к примеру, `pockets.glasses` начнут искать сразу в прототипе (`head`).
          4. -
          \ No newline at end of file diff --git a/1-js/9-prototypes/1-prototype/3-search-algorithm/task.md b/1-js/9-prototypes/1-prototype/3-search-algorithm/task.md deleted file mode 100644 index ab5f9f16..00000000 --- a/1-js/9-prototypes/1-prototype/3-search-algorithm/task.md +++ /dev/null @@ -1,32 +0,0 @@ -# Алгоритм для поиска - -[importance 5] - -Есть объекты: - -```js -var head = { - glasses: 1 -}; - -var table = { - pen: 3 -}; - -var bed = { - sheet: 1, - pillow: 2 -}; - -var pockets = { - money: 2000 -}; -``` - -Задание состоит из двух частей: -
            -
          1. Присвойте объектам ссылки `__proto__` так, чтобы любой поиск чего-либо шёл по алгоритму `pockets -> bed -> table -> head`. - -То есть `pockets.pen == 3`, `bed.glasses == 1`, но `table.money == undefined`.
          2. -
          3. После этого ответьте на вопрос, как быстрее искать `glasses`: обращением к `pockets.glasses` или `head.glasses`? Попробуйте протестировать.
          4. -
          diff --git a/1-js/9-prototypes/1-prototype/article.md b/1-js/9-prototypes/1-prototype/article.md deleted file mode 100644 index 3d83be6a..00000000 --- a/1-js/9-prototypes/1-prototype/article.md +++ /dev/null @@ -1,246 +0,0 @@ -# Прототип объекта - -Объекты в JavaScript можно организовать в цепочки так, чтобы свойство, не найденное в одном объекте, автоматически искалось бы в другом. - -Связующим звеном выступает специальное свойство `__proto__`. - -[cut] -## Прототип __proto__ - -Если один объект имеет специальную ссылку `__proto__` на другой объект, то при чтении свойства из него, если свойство отсутствует в самом объекте, оно ищется в объекте `__proto__`. - -Свойство `__proto__` доступно во всех браузерах, кроме IE10-, а в более старых IE оно, конечно же, тоже есть, но напрямую к нему не обратиться, требуются чуть более сложные способы, которые мы рассмотрим позднее. - -Пример кода (кроме IE10-): - -```js -//+ run -var animal = { - eats: true -}; -var rabbit = { - jumps: true -}; - -*!* -rabbit.__proto__ = animal; -*/!* - -// в rabbit можно найти оба свойства -alert( rabbit.jumps ); // true -alert( rabbit.eats ); // true -``` - -
            -
          1. Первый `alert` здесь работает очевидным образом -- он выводит свойство `jumps` объекта `rabbit`.
          2. -
          3. Второй `alert` хочет вывести `rabbit.eats`, ищет его в самом объекте `rabbit`, не находит -- и продолжает поиск в объекте `rabbit.__proto__`, то есть, в данном случае, в `animal`.
          4. -
          - -Иллюстрация происходящего при чтении `rabbit.eats` (поиск идет снизу вверх): - - - -**Объект, на который указывает ссылка `__proto__`, называется *"прототипом"*. В данном случае получилось, что `animal` является прототипом для `rabbit`.** - -**Также говорят, что объект `rabbit` *"прототипно наследует"* от `animal`.** - -Обратим внимание -- прототип используется исключительно при чтении. Запись значения, например, `rabbit.eats = value` или удаление `delete rabbit.eats` -- работает напрямую с объектом. - -В примере ниже мы записываем свойство в сам `rabbit`, после чего `alert` перестаёт брать его у прототипа, а берёт уже из самого объекта: - -```js -//+ run -var animal = { - eats: true -}; -var rabbit = { - jumps: true, - eats: false -}; - -rabbit.__proto__ = animal; - -*!* -alert( rabbit.eats ); // false, свойство взято из rabbit -*/!* -``` - -**Другими словами, прототип -- это "резервное хранилище свойств и методов" объекта, автоматически используемое при поиске.** - -У объекта, который является `__proto__`, может быть свой `__proto__`, у того -- свой, и так далее. При этом свойства будут искаться по цепочке. - -[smart header="Ссылка __proto__ в спецификации"] -Если вы будете читать спецификацию EcmaScript -- свойство `__proto__` обозначено в ней как `[[Prototype]]`. - -Двойные квадратные скобки здесь важны, чтобы не перепутать его с совсем другим свойством, которое называется `prototype`, и которое мы рассмотрим позже. -[/smart] - - -## Метод hasOwnProperty - -Обычный цикл `for..in` не делает различия между свойствами объекта и его прототипа. - -Он перебирает всё, например: - -```js -//+ run -var animal = { - eats: true -}; - -var rabbit = { - jumps: true, - __proto__: animal -}; - -*!* -for (var key in rabbit) { - alert( key + " = " + rabbit[key] ); // выводит и "eats" и "jumps" -} -*/!* -``` - -Иногда хочется посмотреть, что находится именно в самом объекте, а не в прототипе. - -**Вызов [obj.hasOwnProperty(prop)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/HasOwnProperty) возвращает `true`, если свойство `prop` принадлежит самому объекту `obj`, иначе `false`.** - -Например: - -```js -//+ run -var animal = { - eats: true -}; - -var rabbit = { - jumps: true, - __proto__: animal -}; - -*!* -alert( rabbit.hasOwnProperty('jumps') ); // true: jumps принадлежит rabbit - -alert( rabbit.hasOwnProperty('eats') ); // false: eats не принадлежит -*/!* -``` - -Для того, чтобы перебрать свойства самого объекта, достаточно профильтровать `key` через `hasOwnProperty`: - -```js -//+ run -var animal = { - eats: true -}; - -var rabbit = { - jumps: true, - __proto__: animal -}; - -for (var key in rabbit) { -*!* - if (!rabbit.hasOwnProperty(key)) continue; // пропустить "не свои" свойства -*/!* - alert( key + " = " + rabbit[key] ); // выводит только "jumps" -} -``` - -## Object.create(null) - -Зачастую объекты используют для хранения произвольных значений по ключу, как коллекцию: - -```js -var data = {}; -data.text = "Привет"; -data.age = 35; -// ... -``` - -При дальнейшем поиске в этой коллекции мы найдём не только `text` и `age`, но и встроенные функции: - -```js -//+ run -var data = {}; -alert(data.toString); // функция, хотя мы её туда не записывали -``` - -Это может быть неприятным сюрпризом и приводить к ошибкам, если названия свойств приходят от посетителя и могут быть произвольными. - -Чтобы этого избежать, мы можем исключать свойства, не принадлежащие самому объекту: -```js -//+ run -var data = {}; - -// выведет toString только если оно записано в сам объект -alert(data.hasOwnProperty('toString') ? data.toString : undefined); -``` - -Однако, есть путь и проще: -```js -//+ run -*!* -var data = Object.create(null); -*/!* -data.text = "Привет"; - -alert(data.text); // Привет -*!* -alert(data.toString); // undefined -*/!* -``` - -Объект, создаваемый при помощи `Object.create(null)` не имеет прототипа, а значит в нём нет лишних свойств. Для коллекции -- как раз то, что надо. - - -## Методы для работы с __proto__ - -В современных браузерах есть два дополнительных метода для работы с `__proto__`. Зачем они нужны, если есть `__proto__`? В общем-то, не очень нужны, но по историческим причинам тоже существуют. - -
          -
          Чтение: [Object.getPrototypeOf(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getPrototypeOf)
          -
          Возвращает `obj.__proto__` (кроме IE8-)
          -
          Запись: [Object.setPrototypeOf(obj, proto)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/setPrototypeOf)
          -
          Устанавливает `obj.__proto__ = proto` (кроме IE10-).
          -
          - -Кроме того, есть ещё один вспомогательный метод: -
          Создание объекта с прототипом: [Object.create(proto, descriptors)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/create)
          -
          Создаёт пустой объект с `__proto__`, равным первому аргументу (кроме IE8-), второй необязательный аргумент может содержать [дескрипторы свойств](/descriptors-getters-setters).
          -
      - - - -## Итого - - -
        -
      • В JavaScript есть встроенное "наследование" между объектами при помощи специального свойства `__proto__`.
      • -
      • При установке свойства `rabbit.__proto__ = animal` говорят, что объект `animal` будет "прототипом" `rabbit`.
      • -
      • При чтении свойства из объекта, если его в нём нет, оно ищется в `__proto__`. Прототип задействуется только при чтении свойства. Операции присвоения `obj.prop =` или удаления `delete obj.prop` совершаются всегда над самим объектом `obj`.
      • -
      - -Несколько прототипов одному объекту присвоить нельзя, но можно организовать объекты в цепочку, когда один объект ссылается на другой при помощи `__proto__`, тот ссылается на третий, и так далее. - -В современных браузерах есть методы для работы с прототипом: - -
        -
      • [Object.getPrototypeOf(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getPrototypeOf) (кроме IE8-)
      • -
      • [Object.setPrototypeOf(obj, proto)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/setPrototypeOf) (кроме IE10-)
      • -
      • [Object.create(proto, descriptors)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/create) (кроме IE8-)
      • -
      - -Возможно, вас смущает недостаточная поддержка `__proto__` в старых IE. Но это не страшно. В последующих главах мы рассмотрим дополнительные методы работы с `__proto__`, включая те, которые работают везде. - -Также мы рассмотрим, как свойство `__proto__` используется внутри самого языка JavaScript и как организовать классы с его помощью. - - - -[head] - -[/head] diff --git a/1-js/9-prototypes/1-prototype/proto-animal-rabbit.png b/1-js/9-prototypes/1-prototype/proto-animal-rabbit.png deleted file mode 100644 index 89b49b19..00000000 Binary files a/1-js/9-prototypes/1-prototype/proto-animal-rabbit.png and /dev/null differ diff --git a/1-js/9-prototypes/1-prototype/proto-animal-rabbit@2x.png b/1-js/9-prototypes/1-prototype/proto-animal-rabbit@2x.png deleted file mode 100644 index 2e9e7441..00000000 Binary files a/1-js/9-prototypes/1-prototype/proto-animal-rabbit@2x.png and /dev/null differ diff --git a/1-js/9-prototypes/2-new-prototype/1-prototype-after-new/solution.md b/1-js/9-prototypes/2-new-prototype/1-prototype-after-new/solution.md deleted file mode 100644 index 9256a9fb..00000000 --- a/1-js/9-prototypes/2-new-prototype/1-prototype-after-new/solution.md +++ /dev/null @@ -1,9 +0,0 @@ -Результат: `true`, из прототипа - -Результат: `true`. Свойство `prototype` всего лишь задаёт `__proto__` у новых объектов. Так что его изменение не повлияет на `rabbit.__proto__`. Свойство `eats` будет получено из прототипа. - -Результат: `false`. Свойство `Rabbit.prototype` и `rabbit.__proto__` указывают на один и тот же объект. В данном случае изменения вносятся в сам объект. - -Результат: `true`, так как `delete rabbit.eats` попытается удалить `eats` из `rabbit`, где его и так нет. А чтение в `alert` произойдёт из прототипа. - -Результат: `undefined`. Удаление осуществляется из самого прототипа, поэтому свойство `rabbit.eats` больше взять неоткуда. \ No newline at end of file diff --git a/1-js/9-prototypes/2-new-prototype/1-prototype-after-new/task.md b/1-js/9-prototypes/2-new-prototype/1-prototype-after-new/task.md deleted file mode 100644 index f8ea33a7..00000000 --- a/1-js/9-prototypes/2-new-prototype/1-prototype-after-new/task.md +++ /dev/null @@ -1,89 +0,0 @@ -# Прототип после создания - -[importance 5] - -В примерах ниже создаётся объект `new Rabbit`, а затем проводятся различные действия с `prototype`. - -Каковы будут результаты выполнения? Почему? - -Начнём с этого кода. Что он выведет? - -```js -function Rabbit() {} -Rabbit.prototype = { - eats: true -}; - -var rabbit = new Rabbit(); - -alert( rabbit.eats ); -``` - -Добавили строку (выделена), что будет теперь? - -```js -function Rabbit() {} -Rabbit.prototype = { - eats: true -}; - -var rabbit = new Rabbit(); - -*!* -Rabbit.prototype = {}; -*/!* - -alert( rabbit.eats ); -``` - -А если код будет такой? (заменена одна строка): - -```js -function Rabbit(name) {} -Rabbit.prototype = { - eats: true -}; - -var rabbit = new Rabbit(); - -*!* -Rabbit.prototype.eats = false; -*/!* - -alert( rabbit.eats ); -``` - -А такой? (заменена одна строка) - -```js -function Rabbit(name) {} -Rabbit.prototype = { - eats: true -}; - -var rabbit = new Rabbit(); - -*!* -delete rabbit.eats; // (*) -*/!* - -alert( rabbit.eats ); -``` - -И последний вариант: - -```js -function Rabbit(name) {} -Rabbit.prototype = { - eats: true -}; - -var rabbit = new Rabbit(); - -*!* -delete Rabbit.prototype.eats; // (*) -*/!* - -alert( rabbit.eats ); -``` - diff --git a/1-js/9-prototypes/2-new-prototype/2-default-arguments/solution.md b/1-js/9-prototypes/2-new-prototype/2-default-arguments/solution.md deleted file mode 100644 index 222e9083..00000000 --- a/1-js/9-prototypes/2-new-prototype/2-default-arguments/solution.md +++ /dev/null @@ -1,15 +0,0 @@ -Можно прототипно унаследовать от `options` и добавлять/менять опции в наследнике: - -```js -//+ run -function Menu(options) { - options = Object.create(options); - options.width = options.width || 300; - - alert( options.width ); // возьмёт width из наследника - alert( options.height ); // возьмёт height из исходного объекта - ... -} -``` - -Все изменения будут происходить не в самом `options`, а в его наследнике, при этом исходный объект останется незатронутым. diff --git a/1-js/9-prototypes/2-new-prototype/2-default-arguments/task.md b/1-js/9-prototypes/2-new-prototype/2-default-arguments/task.md deleted file mode 100644 index 1f1c84d5..00000000 --- a/1-js/9-prototypes/2-new-prototype/2-default-arguments/task.md +++ /dev/null @@ -1,27 +0,0 @@ -# Аргументы по умолчанию - -[importance 4] - -Есть функция `Menu`, которая получает аргументы в виде объекта `options`: - -```js -/* options содержит настройки меню: width, height и т.п. */ -function Menu(options) { - ... -} -``` - -Ряд опций должны иметь значение по умолчанию. Мы могли бы проставить их напрямую в объекте `options`: - -```js -function Menu(options) { - options.width = options.width || 300; // по умолчанию ширина 300 - ... -} -``` - -...Но такие изменения могут привести к непредвиденным результатам, т.к. объект `options` может быть повторно использован во внешнем коде. Он передается в `Menu` для того, чтобы параметры из него читали, а не писали. - -Один из способов безопасно назначить значения по умолчанию -- скопировать все свойства `options` в локальные переменные и затем уже менять. Другой способ -- клонировать `options` путём копирования всех свойств из него в новый объект, который уже изменяется. - -При помощи наследования и `Object.create` предложите третий способ, который позволяет избежать копирования объекта и не требует новых переменных. diff --git a/1-js/9-prototypes/2-new-prototype/3-compare-calls/solution.md b/1-js/9-prototypes/2-new-prototype/3-compare-calls/solution.md deleted file mode 100644 index 1b268b84..00000000 --- a/1-js/9-prototypes/2-new-prototype/3-compare-calls/solution.md +++ /dev/null @@ -1,33 +0,0 @@ -# Разница между вызовами - -Первый вызов ставит `this == rabbit`, остальные ставят `this` равным `Rabbit.prototype`, следуя правилу "`this` -- объект перед точкой". - -Так что только первый вызов выведет `Rabbit`, в остальных он будет `undefined`. - -Код для проверки: - -```js -//+ run -function Rabbit(name) { - this.name = name; -} -Rabbit.prototype.sayHi = function() { - alert( this.name ); -} - -var rabbit = new Rabbit("Rabbit"); - -rabbit.sayHi(); -Rabbit.prototype.sayHi(); -Object.getPrototypeOf(rabbit).sayHi(); -rabbit.__proto__.sayHi(); -``` - -# Совместимость - -
        -
      1. Первый вызов работает везде.
      2. -
      3. Второй вызов работает везде.
      4. -
      5. Третий вызов не будет работать в IE8-, там нет метода `getPrototypeOf`
      6. -
      7. Четвёртый вызов -- самый "несовместимый", он не будет работать в IE10-, ввиду отсутствия свойства `__proto__`.
      8. -
      \ No newline at end of file diff --git a/1-js/9-prototypes/2-new-prototype/3-compare-calls/task.md b/1-js/9-prototypes/2-new-prototype/3-compare-calls/task.md deleted file mode 100644 index b3773dfa..00000000 --- a/1-js/9-prototypes/2-new-prototype/3-compare-calls/task.md +++ /dev/null @@ -1,27 +0,0 @@ -# Есть ли разница между вызовами? - -[importance 5] - -Создадим новый объект, вот такой: - -```js -function Rabbit(name) { - this.name = name; -} -Rabbit.prototype.sayHi = function() { - alert( this.name ); -} - -var rabbit = new Rabbit("Rabbit"); -``` - -Одинаково ли сработают эти вызовы? - -```js -rabbit.sayHi(); -Rabbit.prototype.sayHi(); -Object.getPrototypeOf(rabbit).sayHi(); -rabbit.__proto__.sayHi(); -``` - -Все ли они являются кросс-браузерными? Если нет -- в каких браузерах сработает каждый? \ No newline at end of file diff --git a/1-js/9-prototypes/2-new-prototype/4-new-object-same-constructor/solution.md b/1-js/9-prototypes/2-new-prototype/4-new-object-same-constructor/solution.md deleted file mode 100644 index eae8b524..00000000 --- a/1-js/9-prototypes/2-new-prototype/4-new-object-same-constructor/solution.md +++ /dev/null @@ -1,34 +0,0 @@ -Да, можем, но только если уверены, что кто-то позаботился о том, чтобы значение `constructor` было верным. - -В частности, без вмешательства в прототип код точно работает, например: - -```js -//+ run -function User(name) { - this.name = name; -} - -var obj = new User('Вася'); -var obj2 = new obj.constructor('Петя'); - -alert( obj2.name ); // Петя (сработало) -``` - -Сработало, так как `User.prototype.constructor == User`. - -Но если кто-то, к примеру, перезапишет `User.prototype` и забудет указать `constructor`, то такой фокус не пройдёт, например: - -```js -//+ run -function User(name) { - this.name = name; - } -*!* -User.prototype = {}; -*/!* - -var obj = new User('Вася'); -var obj2 = new obj.constructor('Петя'); - -alert( obj2.name ); // undefined -``` \ No newline at end of file diff --git a/1-js/9-prototypes/2-new-prototype/4-new-object-same-constructor/task.md b/1-js/9-prototypes/2-new-prototype/4-new-object-same-constructor/task.md deleted file mode 100644 index 99cb4e90..00000000 --- a/1-js/9-prototypes/2-new-prototype/4-new-object-same-constructor/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Создать объект тем же конструктором - -[importance 5] - -Пусть у нас есть произвольный объект `obj`, созданный каким-то конструктором, каким -- мы не знаем, но хотели бы создать новый объект с его помощью. - -Сможем ли мы сделать так? - -```js -var obj2 = new obj.constructor(); -``` - -Приведите пример конструкторов для `obj`, при которых такой код будет работать верно -- и неверно. diff --git a/1-js/9-prototypes/2-new-prototype/article.md b/1-js/9-prototypes/2-new-prototype/article.md deleted file mode 100644 index 70120f02..00000000 --- a/1-js/9-prototypes/2-new-prototype/article.md +++ /dev/null @@ -1,229 +0,0 @@ -# Свойство F.prototype и создание объектов через new - -До этого момента мы говорили о наследовании объектов, объявленных через `{...}`. - -Но в реальных проектах объекты обычно создаются функцией-конструктором через `new`. Посмотрим, как указать прототип в этом случае. -[cut] - -## Свойство F.prototype - -Самым очевидным решением является назначение `__proto__` в конструкторе. - -Например, если я хочу, чтобы у всех объектов, которые создаются `new Rabbit`, был прототип `animal`, я могу сделать так: - -```js -//+ run -var animal = { - eats: true -}; - -function Rabbit(name) { - this.name = name; -*!* - this.__proto__ = animal; -*/!* -} - -var rabbit = new Rabbit("Кроль"); - -alert( rabbit.eats ); // true, из прототипа -``` - -Недостаток этого подхода -- он не работает в IE10-. - -К счастью, в JavaScript с древнейших времён существует альтернативный, встроенный в язык и полностью кросс-браузерный способ. - -**Чтобы новым объектам автоматически ставить прототип, конструктору ставится свойство `prototype`.** - -**При создании объекта через `new`, в его прототип `__proto__` записывается ссылка из `prototype` функции-конструктора.** - -Например, код ниже полностью аналогичен предыдущему, но работает всегда и везде: - -```js -//+ run -var animal = { - eats: true -}; - -function Rabbit(name) { - this.name = name; -} - -*!* -Rabbit.prototype = animal; -*/!* - -var rabbit = new Rabbit("Кроль"); // rabbit.__proto__ == animal - -alert( rabbit.eats ); // true -``` - -Установка `Rabbit.prototype = animal` буквально говорит интерпретатору следующее: *"При создании объекта через `new Rabbit` запиши ему `__proto__ = animal`".* - -[smart header="Свойство `prototype` имеет смысл только у конструктора"] -Свойство с именем `prototype` можно указать на любом объекте, но особый смысл оно имеет, лишь если назначено функции-конструктору. - -Само по себе, без вызова оператора `new`, оно вообще ничего не делает, его единственное назначение -- указывать `__proto__` для новых объектов. -[/smart] - - - -[warn header="Значением `prototype` может быть только объект"] -Технически, в это свойство можно записать что угодно. - -Однако, при работе `new`, свойство `prototype` будет использовано лишь в том случае, если это объект. Примитивное значение, такое как число или строка, будет проигнорировано. -[/warn] - -## Свойство constructor - -У каждой функции по умолчанию уже есть свойство `prototype`. - -Оно содержит объект такого вида: - -```js -function Rabbit() {} - -Rabbit.prototype = { - constructor: Rabbit -}; -``` - -В коде выше я создал `Rabbit.prototype` вручную, но ровно такой же -- генерируется автоматически. - -Проверим: - -```js -//+ run -function Rabbit() {} - -// в Rabbit.prototype есть одно свойство: constructor -alert( Object.getOwnPropertyNames(Rabbit.prototype) ); // constructor - -// оно равно Rabbit -alert( Rabbit.prototype.constructor == Rabbit ); // true -``` - -Можно его использовать для создания объекта с тем же конструктором, что и данный: - -```js -//+ run -function Rabbit(name) { - this.name = name; - alert( name ); -} - -var rabbit = new Rabbit("Кроль"); - -var rabbit2 = new rabbit.constructor("Крольчиха"); -``` - -Эта возможность бывает полезна, когда, получив объект, мы не знаем в точности, какой у него был конструктор (например, сделан вне нашего кода), а нужно создать такой же. - -[warn header="Свойство `constructor` легко потерять"] -JavaScript никак не использует свойство `constructor`. То есть, оно создаётся автоматически, а что с ним происходит дальше -- это уже наша забота. В стандарте прописано только его создание. - -В частности, при перезаписи `Rabbit.prototype = { jumps: true }` свойства `constructor` больше не будет. - -Сам интерпретатор JavaScript его в служебных целях не требует, поэтому в работе объектов ничего не "сломается". Но если мы хотим, чтобы возможность получить конструктор, всё же, была, то можно при перезаписи гарантировать наличие `constructor` вручную: -```js -Rabbit.prototype = { - jumps: true, -*!* - constructor: Rabbit -*/!* -}; -``` - -Либо можно поступить аккуратно и добавить свойства к встроенному `prototype` без его замены: -```js -// сохранится встроенный constructor -Rabbit.prototype.jumps = true -``` -[/warn] - - -## Эмуляция Object.create для IE8- [#inherit] - -Как мы только что видели, с конструкторами всё просто, назначить прототип можно кросс-браузерно при помощи `F.prototype`. - -Теперь небольшое "лирическое отступление" в область совместимости. - -Прямые методы работы с прототипом осутствуют в старых IE, но один из них -- `Object.create(proto)` можно эмулировать, как раз при помощи `prototype`. И он будет работать везде, даже в самых устаревших браузерах. - -Кросс-браузерный аналог -- назовём его `inherit`, состоит буквально из нескольких строк: - -```js -function inherit(proto) { - function F() {} - F.prototype = proto; - var object = new F; - return object; -} -``` - -Результат вызова `inherit(animal)` идентичен `Object.create(animal)`. Она создаёт новый пустой объект с прототипом `animal`. - -Например: - -```js -//+ run -var animal = { - eats: true -}; - -var rabbit = inherit(animal); - -alert( rabbit.eats ); // true -``` - -Посмотрите внимательно на функцию `inherit` и вы, наверняка, сами поймёте, как она работает... - -Если где-то неясности, то её построчное описание: - -```js -//+ no-beautify -function inherit(proto) { - function F() {} // (1) - F.prototype = proto // (2) - var object = new F; // (3) - return object; // (4) -} -``` - -
        -
      1. Создана новая функция `F`. Она ничего не делает с `this`, так что если вызвать `new F`, то получим пустой объект.
      2. -
      3. Свойство `F.prototype` устанавливается в будущий прототип `proto`
      4. -
      5. Результатом вызова `new F` будет пустой объект с `__proto__` равным значению `F.prototype`.
      6. -
      7. Мы получили пустой объект с заданным прототипом, как и хотели. Возвратим его.
      8. -
      - -Для унификации можно запустить такой код, и метод `Object.create` станет кросс-браузерным: - -```js -if (!Object.create) Object.create = inherit; /* определение inherit - выше */ -``` - -В частности, аналогичным образом работает библиотека [es5-shim](https://github.com/es-shims/es5-shim), при подключении которой `Object.create` станет доступен для всех браузеров. - - -## Итого - -Для произвольной функции -- назовём её `Constructor`, верно следующее: - -
        -
      • Прототип `__proto__` новых объектов, создаваемых через `new Constructor`, можно задавать при помощи свойства `Constructor.prototype`.
      • -
      • Значением `Constructor.prototype` по умолчанию является объект с единственным свойством `constructor`, содержащим ссылку на `Constructor`. Его можно использовать, чтобы из самого объекта получить функцию, которая его создала. Однако, JavaScript никак не поддерживает корректность этого свойства, поэтому программист может его изменить или удалить.
      • -
      • Современный метод `Object.create(proto)` можно эмулировать при помощи `prototype`, если хочется, чтобы он работал в IE8-.
      • -
      - - - -[head] - -[/head] \ No newline at end of file diff --git a/1-js/9-prototypes/3-native-prototypes/1-defer-to-prototype/solution.md b/1-js/9-prototypes/3-native-prototypes/1-defer-to-prototype/solution.md deleted file mode 100644 index 46d09410..00000000 --- a/1-js/9-prototypes/3-native-prototypes/1-defer-to-prototype/solution.md +++ /dev/null @@ -1,15 +0,0 @@ - - -```js -//+ run -Function.prototype.defer = function(ms) { - setTimeout(this, ms); -} - -function f() { - alert( "привет" ); -} - -f.defer(1000); // выведет "привет" через 1 секунду -``` - diff --git a/1-js/9-prototypes/3-native-prototypes/1-defer-to-prototype/task.md b/1-js/9-prototypes/3-native-prototypes/1-defer-to-prototype/task.md deleted file mode 100644 index 000adcc5..00000000 --- a/1-js/9-prototypes/3-native-prototypes/1-defer-to-prototype/task.md +++ /dev/null @@ -1,16 +0,0 @@ -# Добавить функциям defer - -[importance 5] - -Добавьте всем функциям в прототип метод `defer(ms)`, который откладывает вызов функции на `ms` миллисекунд. - -После этого должен работать такой код: - -```js -function f() { - alert( "привет" ); -} - -f.defer(1000); // выведет "привет" через 1 секунду -``` - diff --git a/1-js/9-prototypes/3-native-prototypes/2-defer-to-prototype-extended/solution.md b/1-js/9-prototypes/3-native-prototypes/2-defer-to-prototype-extended/solution.md deleted file mode 100644 index 6c421265..00000000 --- a/1-js/9-prototypes/3-native-prototypes/2-defer-to-prototype-extended/solution.md +++ /dev/null @@ -1,23 +0,0 @@ - - -```js -//+ run -Function.prototype.defer = function(ms) { - var f = this; - return function() { - var args = arguments, - context = this; - setTimeout(function() { - f.apply(context, args); - }, ms); - } -} - -// проверка -function f(a, b) { - alert( a + b ); -} - -f.defer(1000)(1, 2); // выведет 3 через 1 секунду. -``` - diff --git a/1-js/9-prototypes/3-native-prototypes/2-defer-to-prototype-extended/task.md b/1-js/9-prototypes/3-native-prototypes/2-defer-to-prototype-extended/task.md deleted file mode 100644 index ddb3d4ba..00000000 --- a/1-js/9-prototypes/3-native-prototypes/2-defer-to-prototype-extended/task.md +++ /dev/null @@ -1,19 +0,0 @@ -# Добавить функциям defer с аргументами - -[importance 4] - -Добавьте всем функциям в прототип метод defer(ms), который возвращает обёртку, откладывающую вызов функции на ms миллисекунд. - -Например, должно работать так: - -```js -function f(a, b) { - alert( a + b ); -} - -f.defer(1000)(1, 2); // выведет 3 через 1 секунду. -``` - -То есть, должны корректно передаваться аргументы. - - diff --git a/1-js/9-prototypes/3-native-prototypes/article.md b/1-js/9-prototypes/3-native-prototypes/article.md deleted file mode 100644 index 1c8ce795..00000000 --- a/1-js/9-prototypes/3-native-prototypes/article.md +++ /dev/null @@ -1,328 +0,0 @@ -# Встроенные "классы" в JavaScript - -В JavaScript есть встроенные объекты: `Date`, `Array`, `Object` и другие. Они используют прототипы и демонстрируют организацию "псевдоклассов" на JavaScript, которую мы вполне можем применить и для себя. - -[cut] - -## Откуда методы у {} ? - -Начнём мы с того, что создадим пустой объект и выведем его. - -```js -//+ run -var obj = {}; -alert( obj ); // "[object Object]" ? -``` - -Где код, который генерирует строковое представление для `alert(obj)`? Объект-то ведь пустой. - -## Object.prototype - -...Конечно же, это сделал метод `toString`, который находится... Конечно, не в самом объекте (он пуст), а в его прототипе `obj.__proto__`, можно его даже вывести: - -```js -//+ run -alert( {}.__proto__.toString ); // function toString -``` - -Откуда новый объект `obj` получает такой `__proto__`? - -
        -
      1. Запись `obj = {}` является краткой формой `obj = new Object`, где `Object` -- встроенная функция-конструктор для объектов.
      2. -
      3. При выполнении `new Object`, создаваемому объекту ставится `__proto__` по `prototype` конструктора, который в данном случае равен встроенному `Object.prototype`.
      4. -
      5. В дальнейшем при обращении к `obj.toString()` -- функция будет взята из `Object.prototype`.
      6. -
      - - - -Это можно легко проверить: - -```js -//+ run -var obj = {}; - -// метод берётся из прототипа? -alert( obj.toString == Object.prototype.toString ); // true, да - -// проверим, правда ли что __proto__ это Object.prototype? -alert( obj.__proto__ == Object.prototype ); // true - -// А есть ли __proto__ у Object.prototype? -alert( obj.__proto__.__proto__ ); // null, нет -``` - -## Встроенные "классы" в JavaScript - -Точно такой же подход используется в массивах `Array`, функциях `Function` и других объектах. Встроенные методы для них находятся в `Array.prototype`, `Function.prototype` и т.п. - - - -Например, когда мы создаём массив, `[1, 2, 3]`, то это альтернативный вариант синтаксиса `new Array`, так что у массивов есть стандартный прототип `Array.prototype`. - -Но в нём есть методы лишь для массивов, а для общих методов всех объектов есть ссылка `Array.prototype.__proto__`, равная `Object.prototype`. - -Аналогично, для функций. - -Лишь для чисел (как и других примитивов) всё немного иначе, но об этом чуть далее. - -Объект `Object.prototype` -- вершина иерархии, единственный, у которого `__proto__` равно `null`. - -**Поэтому говорят, что "все объекты наследуют от `Object`", а если более точно, то от `Object.prototype`.** - -"Псевдоклассом" или, более коротко, "классом", называют функцию-конструктор вместе с её `prototype`. Такой способ объявления классов называют "прототипным стилем ООП". - -При наследовании часть методов переопределяется, например, у массива `Array` есть свой `toString`, который выводит элементы массива через запятую: - -```js -//+ run -var arr = [1, 2, 3] -alert( arr ); // 1,2,3 <-- результат Array.prototype.toString -``` - -Как мы видели раньше, у `Object.prototype` есть свой `toString`, но так как в `Array.prototype` он ищется первым, то берётся именно вариант для массивов: - - - - -[smart header="Вызов методов через `apply` из прототипа"] - -Ранее мы говорили о применении методов массивов к "псевдомассивам", например, можно использовать `[].join` для `arguments`: - -```js -//+ run -function showList() { -*!* - alert( [].join.call(arguments, " - ") ); -*/!* -} - -showList("Вася", "Паша", "Маша"); // Вася - Паша - Маша -``` - -Так как метод `join` находится в `Array.prototype`, то можно вызвать его оттуда напрямую, вот так: - -```js -//+ run -function showList() { -*!* - alert( Array.prototype.join.call(arguments, " - ") ); -*/!* -} - -showList("Вася", "Паша", "Маша"); // Вася - Паша - Маша -``` - -Это эффективнее, потому что не создаётся лишний объект массива `[]`, хотя, с другой стороны -- больше букв писать. -[/smart] - -## Примитивы - -Примитивы не являются объектами, но методы берут из соответствующих прототипов: `Number.prototype`, `Boolean.prototype`, `String.prototype`. - -По стандарту, если обратиться к свойству числа, строки или логического значения, то будет создан объект соответствующего типа, например `new String` для строки, `new Number` для чисел, `new Boolean` -- для логических выражений. - -Далее будет произведена операция со свойством или вызов метода по обычным правилам, с поиском в прототипе, а затем этот объект будет уничтожен. - -Именно так работает код ниже: - -```js -//+ run -var user = "Вася"; // создали строку (примитив) - -*!* -alert( user.toUpperCase() ); // ВАСЯ -// был создан временный объект new String -// вызван метод -// new String уничтожен, результат возвращён -*/!* -``` - -Можно даже попробовать записать в этот временный объект свойство: - -```js -//+ run -// попытаемся записать свойство в строку: -var user = "Вася"; -user.age = 30; - -*!* -alert( user.age ); // undefined -*/!* -``` - -Свойство `age` было записано во временный объект, который был тут же уничтожен, так что смысла в такой записи немного. - -[warn header="Конструкторы `String/Number/Boolean` -- только для внутреннего использования"] -Технически, можно создавать объекты для примитивов и вручную, например `new Number`. Но в ряде случаев получится откровенно бредовое поведение. Например: - -```js -//+ run -alert( typeof 1 ); // "number" - -alert( typeof new Number(1) ); // "object" ?!? -``` - -Или, ещё страннее: - -```js -//+ run -var zero = new Number(0); - -if (zero) { // объект - true, так что alert выполнится - alert( "число ноль -- true?!?" ); -} -``` - -Поэтому в явном виде `new String`, `new Number` и `new Boolean` никогда не вызываются. -[/warn] - -[warn header="Значения `null` и `undefined` не имеют свойств"] -Значения `null` и `undefined` стоят особняком. Вышесказанное к ним не относится. - -Для них нет соответствующих классов, в них нельзя записать свойство (будет ошибка), в общем, на конкурсе "самое примитивное значение" они точно разделили бы первое место. -[/warn] - - -## Изменение встроенных прототипов [#native-prototype-change] - -Встроенные прототипы можно изменять. В том числе -- добавлять свои методы. - -Мы можем написать метод для многократного повторения строки, и он тут же станет доступным для всех строк: - -```js -//+ run -String.prototype.repeat = function(times) { - return new Array(times + 1).join(this); -}; - -alert( "ля".repeat(3) ); // ляляля -``` - -Аналогично мы могли бы создать метод `Object.prototype.each(func)`, который будет применять `func` к каждому свойству: - -```js -//+ run -Object.prototype.each = function(f) { - for (var prop in this) { - var value = this[prop]; - f.call(value, prop, value); // вызовет f(prop, value), this=value - } -} - -// Попробуем! (внимание, пока что это работает неверно!) -var user = { - name: 'Вася', - age: 25 -}; - -user.each(function(prop, val) { - alert( prop ); // name -> age -> (!) each -}); -``` - -Обратите внимание -- пример выше работает не совсем корректно. Вместе со свойствами объекта `user` он выводит и наше свойство `each`. Технически, это правильно, так как цикл `for..in` перебирает свойства и в прототипе тоже, но не очень удобно. - -Конечно, это легко поправить добавлением проверки `hasOwnProperty`: - -```js -//+ run -Object.prototype.each = function(f) { - - for (var prop in this) { - -*!* - // пропускать свойства из прототипа - if (!this.hasOwnProperty(prop)) continue; -*/!* - - var value = this[prop]; - f.call(value, prop, value); - - } - -}; - -// Теперь все будет в порядке -var obj = { - name: 'Вася', - age: 25 -}; - -obj.each(function(prop, val) { - alert( prop ); // name -> age -}); -``` - -Здесь это сработало, теперь код работает верно. Но мы же не хотим добавлять `hasOwnProperty` в цикл по любому объекту! Поэтому либо не добавляйте свойства в `Object.prototype`, либо можно использовать [дескриптор свойства](/descriptors-getters-setters) и флаг `enumerable`. - -Это, конечно, не будет работать в IE8-: - -```js -//+ run -Object.prototype.each = function(f) { - - for (var prop in this) { - var value = this[prop]; - f.call(value, prop, value); - } - -}; - -*!* -// поправить объявление свойства, установив флаг enumerable: false -Object.defineProperty(Object.prototype, 'each', { - enumerable: false -}); -*/!* - -// Теперь все будет в порядке -var obj = { - name: 'Вася', - age: 25 -}; - -obj.each(function(prop, val) { - alert( prop ); // name -> age -}); -``` - -Есть несколько "за" и "против" модификации встроенных прототипов: - -[compare] -+Методы в прототипе автоматически доступны везде, их вызов прост и красив. --Новые свойства, добавленные в прототип из разных мест, могут конфликтовать между собой. Представьте, что вы подключили две библиотеки, которые добавили одно и то же свойство в прототип, но определили его по-разному. Конфликт неизбежен. --Изменения встроенных прототипов влияют глобально, на все-все скрипты, делать их не очень хорошо с архитектурной точки зрения. -[/compare] - -Как правило, минусы весомее, но есть одно исключение, когда изменения встроенных прототипов не только разрешены, но и приветствуются. - -**Допустимо изменение прототипа встроенных объектов, которое добавляет поддержку метода из современных стандартов в те браузеры, где её пока нет.** - -Например, добавим `Object.create(proto)` в старые браузеры: - -```js -if (!Object.create) { - - Object.create = function(proto) { - function F() {} - F.prototype = proto; - return new F; - }; - -} -``` - -Именно так работает библиотека [es5-shim](https://github.com/kriskowal/es5-shim), которая предоставляет многие функции современного JavaScript для старых браузеров. Они добавляются во встроенные объекты и их прототипы. - -## Итого - -
        -
      • Методы встроенных объектов хранятся в их прототипах.
      • -
      • Встроенные прототипы можно расширить или поменять.
      • -
      • Добавление методов в `Object.prototype`, если оно не сопровождается `Object.defineProperty` с установкой `enumerable` (IE9+), "сломает" циклы `for..in`, поэтому стараются в этот прототип методы не добавлять. - -Другие прототипы изменять менее опасно, но все же не рекомендуется во избежание конфликтов. - -Отдельно стоит изменение с целью добавления современных методов в старые браузеры, таких как Object.create, Object.keys, Function.prototype.bind и т.п. Это допустимо и как раз делается [es5-shim](https://github.com/kriskowal/es5-shim).
      • -
      - diff --git a/1-js/9-prototypes/3-native-prototypes/native-prototype-object.png b/1-js/9-prototypes/3-native-prototypes/native-prototype-object.png deleted file mode 100644 index 1353bd9c..00000000 --- a/1-js/9-prototypes/3-native-prototypes/native-prototype-object.png +++ /dev/null @@ -1 +0,0 @@ -native-prototype-objecttoString: function другие методы объектовObject.prototypeobj__proto____proto__null \ No newline at end of file diff --git a/1-js/9-prototypes/3-native-prototypes/native-prototypes-array-tostring.png b/1-js/9-prototypes/3-native-prototypes/native-prototypes-array-tostring.png deleted file mode 100644 index 0c6f18e5..00000000 Binary files a/1-js/9-prototypes/3-native-prototypes/native-prototypes-array-tostring.png and /dev/null differ diff --git a/1-js/9-prototypes/3-native-prototypes/native-prototypes-array-tostring@2x.png b/1-js/9-prototypes/3-native-prototypes/native-prototypes-array-tostring@2x.png deleted file mode 100644 index 6f58e781..00000000 Binary files a/1-js/9-prototypes/3-native-prototypes/native-prototypes-array-tostring@2x.png and /dev/null differ diff --git a/1-js/9-prototypes/3-native-prototypes/native-prototypes-classes.png b/1-js/9-prototypes/3-native-prototypes/native-prototypes-classes.png deleted file mode 100644 index 451f4548..00000000 Binary files a/1-js/9-prototypes/3-native-prototypes/native-prototypes-classes.png and /dev/null differ diff --git a/1-js/9-prototypes/3-native-prototypes/native-prototypes-classes@2x.png b/1-js/9-prototypes/3-native-prototypes/native-prototypes-classes@2x.png deleted file mode 100644 index 28783af2..00000000 Binary files a/1-js/9-prototypes/3-native-prototypes/native-prototypes-classes@2x.png and /dev/null differ diff --git a/1-js/9-prototypes/3-native-prototypes/native-prototypes-object.png b/1-js/9-prototypes/3-native-prototypes/native-prototypes-object.png deleted file mode 100644 index 5ac3315e..00000000 Binary files a/1-js/9-prototypes/3-native-prototypes/native-prototypes-object.png and /dev/null differ diff --git a/1-js/9-prototypes/3-native-prototypes/native-prototypes-object@2x.png b/1-js/9-prototypes/3-native-prototypes/native-prototypes-object@2x.png deleted file mode 100644 index 26ae7911..00000000 Binary files a/1-js/9-prototypes/3-native-prototypes/native-prototypes-object@2x.png and /dev/null differ diff --git a/1-js/9-prototypes/4-classes/1-rewrite-by-class/solution.md b/1-js/9-prototypes/4-classes/1-rewrite-by-class/solution.md deleted file mode 100644 index a43508c8..00000000 --- a/1-js/9-prototypes/4-classes/1-rewrite-by-class/solution.md +++ /dev/null @@ -1,32 +0,0 @@ - - -```js -//+ run -function CoffeeMachine(power) { - // свойства конкретной кофеварки - this._power = power; - this._waterAmount = 0; -} - -// свойства и методы для всех объектов класса -CoffeeMachine.prototype.WATER_HEAT_CAPACITY = 4200; - -CoffeeMachine.prototype._getTimeToBoil = function() { - return this._waterAmount * this.WATER_HEAT_CAPACITY * 80 / this._power; -}; - -CoffeeMachine.prototype.run = function() { - setTimeout(function() { - alert( 'Кофе готов!' ); - }, this._getTimeToBoil()); -}; - -CoffeeMachine.prototype.setWaterAmount = function(amount) { - this._waterAmount = amount; -}; - -var coffeeMachine = new CoffeeMachine(10000); -coffeeMachine.setWaterAmount(50); -coffeeMachine.run(); -``` - diff --git a/1-js/9-prototypes/4-classes/1-rewrite-by-class/task.md b/1-js/9-prototypes/4-classes/1-rewrite-by-class/task.md deleted file mode 100644 index cfc8aa91..00000000 --- a/1-js/9-prototypes/4-classes/1-rewrite-by-class/task.md +++ /dev/null @@ -1,39 +0,0 @@ -# Перепишите в виде класса - -[importance 5] - -Есть класс `CoffeeMachine`, заданный в функциональном стиле. - -Задача: переписать `CoffeeMachine` в виде класса с использованием прототипа. - -Исходный код: - -```js -//+ run -function CoffeeMachine(power) { - var waterAmount = 0; - - var WATER_HEAT_CAPACITY = 4200; - - function getTimeToBoil() { - return waterAmount * WATER_HEAT_CAPACITY * 80 / power; - } - - this.run = function() { - setTimeout(function() { - alert( 'Кофе готов!' ); - }, getTimeToBoil()); - }; - - this.setWaterAmount = function(amount) { - waterAmount = amount; - }; - -} - -var coffeeMachine = new CoffeeMachine(10000); -coffeeMachine.setWaterAmount(50); -coffeeMachine.run(); -``` - -P.S. При описании через прототипы локальные переменные недоступны методам, поэтому нужно будет переделать их в защищённые свойства. diff --git a/1-js/9-prototypes/4-classes/2-hamsters-with-proto/solution.md b/1-js/9-prototypes/4-classes/2-hamsters-with-proto/solution.md deleted file mode 100644 index 113334fd..00000000 --- a/1-js/9-prototypes/4-classes/2-hamsters-with-proto/solution.md +++ /dev/null @@ -1,52 +0,0 @@ -# Почему возникает проблема - -Давайте подробнее разберем происходящее при вызове `speedy.found("яблоко")`: -
        -
      1. Интерпретатор ищет свойство `found` в `speedy`. Но `speedy` -- пустой объект, т.к. `new Hamster` ничего не делает с `this`.
      2. -
      3. Интерпретатор идёт по ссылке `speedy.__proto__ (==Hamster.prototype)` и находят там метод `found`, запускает его.
      4. -
      5. Значение `this` устанавливается в объект перед точкой, т.е. в `speedy`.
      6. -
      7. Для выполнения `this.food.push()` нужно найти свойство `this.food`. Оно отсутствует в `speedy`, но есть в `speedy.__proto__`.
      8. -
      9. Значение `"яблоко"` добавляется в `speedy.__proto__.food`.
      10. -
      - -**У всех хомяков общий живот!** Или, в терминах JavaScript, свойство `food` изменяется в прототипе, который является общим для всех объектов-хомяков. - -Заметим, что этой проблемы не было бы при простом присваивании: - -```js -this.food = something; -``` - -В этом случае значение записалось бы в сам объект, без поиска `found` в прототипе. - -**Проблема возникает только со свойствами-объектами в прототипе.** - -Исправьте её? - -# Исправление - -Для исправления проблемы нужно дать каждому хомяку свой живот. Это можно сделать, присвоив его в конструкторе. - -```js -//+ run -function Hamster() { -*!* - this.food = []; -*/!* -} - -Hamster.prototype.found = function(something) { - this.food.push(something); -}; - -speedy = new Hamster(); -lazy = new Hamster(); - -speedy.found("яблоко"); -speedy.found("орех"); - -alert(speedy.food.length) // 2 -alert(lazy.food.length) // 0(!) -``` - -Теперь всё в порядке. У каждого хомяка -- свой живот. \ No newline at end of file diff --git a/1-js/9-prototypes/4-classes/2-hamsters-with-proto/task.md b/1-js/9-prototypes/4-classes/2-hamsters-with-proto/task.md deleted file mode 100644 index 48aa3220..00000000 --- a/1-js/9-prototypes/4-classes/2-hamsters-with-proto/task.md +++ /dev/null @@ -1,33 +0,0 @@ -# Хомяки с __proto__ - -[importance 5] - -Вы -- руководитель команды, которая разрабатывает игру, хомяковую ферму. Один из программистов получил задание создать класс "хомяк" (англ - `"Hamster"`). - -Объекты-хомяки должны иметь массив `food` для хранения еды и метод `found`, который добавляет к нему. - -Ниже -- его решение. При создании двух хомяков, если поел один -- почему-то сытым становится и второй тоже. - -В чём дело? Как поправить? - -```js -//+ run -function Hamster() {} - -Hamster.prototype.food = []; // пустой "живот" - -Hamster.prototype.found = function(something) { - this.food.push(something); -}; - -// Создаём двух хомяков и кормим первого -var speedy = new Hamster(); -var lazy = new Hamster(); - -speedy.found("яблоко"); -speedy.found("орех"); - -alert( speedy.food.length ); // 2 -alert( lazy.food.length ); // 2 (!??) -``` - diff --git a/1-js/9-prototypes/4-classes/article.md b/1-js/9-prototypes/4-classes/article.md deleted file mode 100644 index 082c22f2..00000000 --- a/1-js/9-prototypes/4-classes/article.md +++ /dev/null @@ -1,128 +0,0 @@ -# Свои классы на прототипах - -Используем ту же структуру, что JavaScript использует внутри себя, для объявления своих классов. - -[cut] -## Обычный конструктор - -Вспомним, как мы объявляли классы ранее. - -Например, этот код задаёт класс `Animal` в функциональном стиле, без всяких прототипов: - -```js -//+ run -function Animal(name) { - this.speed = 0; - this.name = name; - - this.run = function(speed) { - this.speed += speed; - alert( this.name + ' бежит, скорость ' + this.speed ); - }; - - this.stop = function() { - this.speed = 0; - alert( this.name + ' стоит' ); - }; -}; - -var animal = new Animal('Зверь'); - -alert( animal.speed ); // 0, начальная скорость -animal.run(3); // Зверь бежит, скорость 3 -animal.run(10); // Зверь бежит, скорость 13 -animal.stop(); // Зверь стоит -``` - -## Класс через прототип - -А теперь создадим аналогичный класс, используя прототипы, наподобие того, как сделаны классы `Object`, `Date` и остальные. - -Чтобы объявить свой класс, нужно: - -
        -
      1. Объявить функцию-конструктор.
      2. -
      3. Записать методы и свойства, нужные всем объектам класса, в `prototype`.
      4. -
      - -Опишем класс `Animal`: - -```js -//+ run -// конструктор -function Animal(name) { - this.name = name; - this.speed = 0; -} - -// методы в прототипе -Animal.prototype.run = function(speed) { - this.speed += speed; - alert( this.name + ' бежит, скорость ' + this.speed ); -}; - -Animal.prototype.stop = function() { - this.speed = 0; - alert( this.name + ' стоит' ); -}; - -var animal = new Animal('Зверь'); - -alert( animal.speed ); // 0, свойство взято из прототипа -animal.run(5); // Зверь бежит, скорость 5 -animal.run(5); // Зверь бежит, скорость 10 -animal.stop(); // Зверь стоит -``` - -В объекте `animal` будут хранится свойства конкретного экземпляра: `name` и `speed`, а общие методы -- в прототипе. - -Совершенно такой же подход, как и для встроенных классов в JavaScript. - -## Сравнение - -Чем такое задание класса лучше и хуже функционального стиля? - -[compare] -+Функциональный стиль записывает в каждый объект и свойства и методы, а прототипный -- только свойства. Поэтому прототипный стиль -- быстрее и экономнее по памяти. --При создании методов через прототип, мы теряем возможность использовать локальные переменные как приватные свойства, у них больше нет общей области видимости с конструктором. -[/compare] - -Таким образом, прототипный стиль -- быстрее и экономнее, но немного менее удобен. - -К примеру, есть у нас приватное свойство `name` и метод `sayHi` в функциональном стиле ООП: - -```js -//+ run -function Animal(name) { - this.sayHi = function() { -*!* - alert( name ); -*/!* - }; -} - -var animal = new Animal("Зверь"); -animal.sayHi(); // Зверь -``` - -При задании методов в прототипе мы не сможем её так оставить, ведь методы находятся *вне* конструктора, у них нет общей области видимости, поэтому приходится записывать `name` в сам объект, обозначив его как защищённое: - -```js -//+ run -function Animal(name) { -*!* - this._name = name; -*/!* -} - -Animal.prototype.sayHi = function() { -*!* - alert( this._name ); -*/!* -} - -var animal = new Animal("Зверь"); -animal.sayHi(); // Зверь -``` - -Впрочем, недостаток этот -- довольно условный. Ведь при наследовании в функциональном стиле также пришлось бы писать `this._name`, чтобы потомок получил доступ к этому значению. diff --git a/1-js/9-prototypes/5-class-inheritance/1-inheritance-error-assign/solution.md b/1-js/9-prototypes/5-class-inheritance/1-inheritance-error-assign/solution.md deleted file mode 100644 index 8987b2d7..00000000 --- a/1-js/9-prototypes/5-class-inheritance/1-inheritance-error-assign/solution.md +++ /dev/null @@ -1,44 +0,0 @@ -Ошибка в строке: - -```js -Rabbit.prototype = Animal.prototype; -``` - -Эта ошибка приведёт к тому, что `Rabbit.prototype` и `Animal.prototype` -- один и тот же объект. В результате методы `Rabbit` будут помещены в него и, при совпадении, перезапишут методы `Animal`. - -Получится, что все животные прыгают, вот пример: - -```js -//+ run no-beautify -function Animal(name) { - this.name = name; -} - -Animal.prototype.walk = function() { - alert("ходит " + this.name); -}; - -function Rabbit(name) { - this.name = name; -} -*!* -Rabbit.prototype = Animal.prototype; -*/!* - -Rabbit.prototype.walk = function() { - alert("прыгает! и ходит: " + this.name); -}; - -*!* -var animal = new Animal("Хрюшка"); -animal.walk(); // прыгает! и ходит Хрюшка -*/!* -``` - -Правильный вариант этой строки: - -```js -Rabbit.prototype = Object.create(Animal.prototype); -``` - -Если так написать, то в `Rabbit.prototype` будет отдельный объект, который прототипно наследует от `Animal.prototype`, но может содержать и свои свойства, специфичные для кроликов. diff --git a/1-js/9-prototypes/5-class-inheritance/1-inheritance-error-assign/task.md b/1-js/9-prototypes/5-class-inheritance/1-inheritance-error-assign/task.md deleted file mode 100644 index ddb177e9..00000000 --- a/1-js/9-prototypes/5-class-inheritance/1-inheritance-error-assign/task.md +++ /dev/null @@ -1,25 +0,0 @@ -# Найдите ошибку в наследовании - -[importance 5] - -Найдите ошибку в прототипном наследовании. К чему она приведёт? - -```js -function Animal(name) { - this.name = name; -} - -Animal.prototype.walk = function() { - alert( "ходит " + this.name ); -}; - -function Rabbit(name) { - this.name = name; -} -Rabbit.prototype = Animal.prototype; - -Rabbit.prototype.walk = function() { - alert( "прыгает! и ходит: " + this.name ); -}; -``` - diff --git a/1-js/9-prototypes/5-class-inheritance/2-inheritance-error-constructor/solution.md b/1-js/9-prototypes/5-class-inheritance/2-inheritance-error-constructor/solution.md deleted file mode 100644 index 19384566..00000000 --- a/1-js/9-prototypes/5-class-inheritance/2-inheritance-error-constructor/solution.md +++ /dev/null @@ -1,18 +0,0 @@ -Ошибка -- в том, что метод `walk` присваивается в конструкторе `Animal` самому объекту вместо прототипа. - -Поэтому, если мы решим перезаписать этот метод своим, специфичным для кролика, то он не сработает: - -```js -// ... - -// записывается в прототип -Rabbit.prototype.walk = function() { - alert( "прыгает " + this.name ); -}; -``` - -Метод `this.walk` из `Animal` записывается в сам объект, и поэтому он всегда будет первым, игнорируя цепочку прототипов. - -Правильно было бы определять `walk` как `Animal.prototype.walk`. - -Тем более, что этот метод является общим для всех объектов, тратить память и время на запись его в каждый конструктор определённо ни к чему. \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/2-inheritance-error-constructor/task.md b/1-js/9-prototypes/5-class-inheritance/2-inheritance-error-constructor/task.md deleted file mode 100644 index b97447f5..00000000 --- a/1-js/9-prototypes/5-class-inheritance/2-inheritance-error-constructor/task.md +++ /dev/null @@ -1,30 +0,0 @@ -# В чём ошибка в наследовании - -[importance 5] - -Найдите ошибку в прототипном наследовании. К чему она приведёт? - -```js -//+ run -function Animal(name) { - this.name = name; - - this.walk = function() { - alert( "ходит " + this.name ); - }; - -} - -function Rabbit(name) { - Animal.apply(this, arguments); -} -Rabbit.prototype = Object.create(Animal.prototype); - -Rabbit.prototype.walk = function() { - alert( "прыгает " + this.name ); -}; - -var rabbit = new Rabbit("Кроль"); -rabbit.walk(); -``` - diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/clock.js b/1-js/9-prototypes/5-class-inheritance/3-clock-class/clock.js deleted file mode 100755 index 34fb026d..00000000 --- a/1-js/9-prototypes/5-class-inheritance/3-clock-class/clock.js +++ /dev/null @@ -1,32 +0,0 @@ -function Clock(options) { - this._template = options.template; -} - -Clock.prototype._render = function render() { - var date = new Date(); - - var hours = date.getHours(); - if (hours < 10) hours = '0' + hours; - - var min = date.getMinutes(); - if (min < 10) min = '0' + min; - - var sec = date.getSeconds(); - if (sec < 10) sec = '0' + sec; - - var output = this._template.replace('h', hours).replace('m', min).replace('s', sec); - - console.log(output); -}; - -Clock.prototype.stop = function() { - clearInterval(this._timer); -}; - -Clock.prototype.start = function() { - this._render(); - var self = this; - this._timer = setInterval(function() { - self._render(); - }, 1000); -}; \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.md b/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.md deleted file mode 100644 index d1c08b87..00000000 --- a/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.md +++ /dev/null @@ -1,7 +0,0 @@ - - -```js -//+ src="clock.js" -``` - -[edit src="solution"]Открыть полное решение[/edit] \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.view/clock.js b/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.view/clock.js deleted file mode 100755 index 34fb026d..00000000 --- a/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.view/clock.js +++ /dev/null @@ -1,32 +0,0 @@ -function Clock(options) { - this._template = options.template; -} - -Clock.prototype._render = function render() { - var date = new Date(); - - var hours = date.getHours(); - if (hours < 10) hours = '0' + hours; - - var min = date.getMinutes(); - if (min < 10) min = '0' + min; - - var sec = date.getSeconds(); - if (sec < 10) sec = '0' + sec; - - var output = this._template.replace('h', hours).replace('m', min).replace('s', sec); - - console.log(output); -}; - -Clock.prototype.stop = function() { - clearInterval(this._timer); -}; - -Clock.prototype.start = function() { - this._render(); - var self = this; - this._timer = setInterval(function() { - self._render(); - }, 1000); -}; \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.view/index.html b/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.view/index.html deleted file mode 100755 index ec1acda9..00000000 --- a/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.view/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Часики в консоли - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/source.view/clock.js b/1-js/9-prototypes/5-class-inheritance/3-clock-class/source.view/clock.js deleted file mode 100755 index 87f457c6..00000000 --- a/1-js/9-prototypes/5-class-inheritance/3-clock-class/source.view/clock.js +++ /dev/null @@ -1,32 +0,0 @@ -function Clock(options) { - - var template = options.template; - var timer; - - function render() { - var date = new Date(); - - var hours = date.getHours(); - if (hours < 10) hours = '0' + hours; - - var min = date.getMinutes(); - if (min < 10) min = '0' + min; - - var sec = date.getSeconds(); - if (sec < 10) sec = '0' + sec; - - var output = template.replace('h', hours).replace('m', min).replace('s', sec); - - console.log(output); - } - - this.stop = function() { - clearInterval(timer); - }; - - this.start = function() { - render(); - timer = setInterval(render, 1000); - } - -} \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/source.view/index.html b/1-js/9-prototypes/5-class-inheritance/3-clock-class/source.view/index.html deleted file mode 100755 index ec1acda9..00000000 --- a/1-js/9-prototypes/5-class-inheritance/3-clock-class/source.view/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Часики в консоли - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/task.md b/1-js/9-prototypes/5-class-inheritance/3-clock-class/task.md deleted file mode 100644 index 59355fee..00000000 --- a/1-js/9-prototypes/5-class-inheritance/3-clock-class/task.md +++ /dev/null @@ -1,11 +0,0 @@ -# Класс "часы" - -[importance 5] - -Есть реализация часиков, оформленная в виде одной функции-конструктора. У неё есть приватные свойства `timer`, `template` и метод `render`. - -Задача: переписать часы на прототипах. Приватные свойства и методы сделать защищёнными. - - - -P.S. Часики тикают в браузерной консоли (надо открыть её, чтобы увидеть). \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/extended-clock.js b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/extended-clock.js deleted file mode 100755 index 3e0bf9bd..00000000 --- a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/extended-clock.js +++ /dev/null @@ -1,14 +0,0 @@ -function ExtendedClock(options) { - Clock.apply(this, arguments); - this._precision = +options.precision || 1000; -} - -ExtendedClock.prototype = Object.create(Clock.prototype); - -ExtendedClock.prototype.start = function() { - this._render(); - var self = this; - this._timer = setInterval(function() { - self._render(); - }, this._precision); -}; \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.md b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.md deleted file mode 100644 index f58d46d8..00000000 --- a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.md +++ /dev/null @@ -1,7 +0,0 @@ -Наследник: - -```js -//+ src="extended-clock.js" -``` - -[edit src="solution"]Открыть полное решение в редакторе[/edit] \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/clock.js b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/clock.js deleted file mode 100755 index 34fb026d..00000000 --- a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/clock.js +++ /dev/null @@ -1,32 +0,0 @@ -function Clock(options) { - this._template = options.template; -} - -Clock.prototype._render = function render() { - var date = new Date(); - - var hours = date.getHours(); - if (hours < 10) hours = '0' + hours; - - var min = date.getMinutes(); - if (min < 10) min = '0' + min; - - var sec = date.getSeconds(); - if (sec < 10) sec = '0' + sec; - - var output = this._template.replace('h', hours).replace('m', min).replace('s', sec); - - console.log(output); -}; - -Clock.prototype.stop = function() { - clearInterval(this._timer); -}; - -Clock.prototype.start = function() { - this._render(); - var self = this; - this._timer = setInterval(function() { - self._render(); - }, 1000); -}; \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/extended-clock.js b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/extended-clock.js deleted file mode 100755 index 3e0bf9bd..00000000 --- a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/extended-clock.js +++ /dev/null @@ -1,14 +0,0 @@ -function ExtendedClock(options) { - Clock.apply(this, arguments); - this._precision = +options.precision || 1000; -} - -ExtendedClock.prototype = Object.create(Clock.prototype); - -ExtendedClock.prototype.start = function() { - this._render(); - var self = this; - this._timer = setInterval(function() { - self._render(); - }, this._precision); -}; \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/index.html b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/index.html deleted file mode 100755 index c226468c..00000000 --- a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/index.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - Часики в консоли - - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/clock.js b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/clock.js deleted file mode 100755 index 34fb026d..00000000 --- a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/clock.js +++ /dev/null @@ -1,32 +0,0 @@ -function Clock(options) { - this._template = options.template; -} - -Clock.prototype._render = function render() { - var date = new Date(); - - var hours = date.getHours(); - if (hours < 10) hours = '0' + hours; - - var min = date.getMinutes(); - if (min < 10) min = '0' + min; - - var sec = date.getSeconds(); - if (sec < 10) sec = '0' + sec; - - var output = this._template.replace('h', hours).replace('m', min).replace('s', sec); - - console.log(output); -}; - -Clock.prototype.stop = function() { - clearInterval(this._timer); -}; - -Clock.prototype.start = function() { - this._render(); - var self = this; - this._timer = setInterval(function() { - self._render(); - }, 1000); -}; \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/extended-clock.js b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/extended-clock.js deleted file mode 100755 index 3b3efe1e..00000000 --- a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/extended-clock.js +++ /dev/null @@ -1,13 +0,0 @@ -function extend(Child, Parent) { - Child.prototype = inherit(Parent.prototype); - Child.prototype.constructor = Child; - Child.parent = Parent.prototype; -} - -function inherit(proto) { - function F() {} - F.prototype = proto; - return new F; -} - -// ваш код \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/index.html b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/index.html deleted file mode 100755 index 4aac9ceb..00000000 --- a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/index.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - Часики в консоли - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/task.md b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/task.md deleted file mode 100644 index 94bee22a..00000000 --- a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/task.md +++ /dev/null @@ -1,15 +0,0 @@ -# Класс "расширенные часы" - -[importance 5] - -Есть реализация часиков на прототипах. Создайте класс, расширяющий её, добавляющий поддержку параметра `precision`, который будет задавать частоту тика в `setInterval`. Значение по умолчанию: `1000`. - - - -
        -
      • Для этого класс `Clock` надо унаследовать. Пишите ваш новый код в файле `extended-clock.js`.
      • -
      • Исходный класс `Clock` менять нельзя.
      • -
      • Пусть конструктор потомка вызывает конструктор родителя. Это позволит избежать проблем при расширении `Clock` новыми опциями.
      • -
      - -P.S. Часики тикают в браузерной консоли (надо открыть её, чтобы увидеть). \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.md b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.md deleted file mode 100644 index 9cb942f7..00000000 --- a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.md +++ /dev/null @@ -1,3 +0,0 @@ -[edit src="solution"]Открыть решение в редакторе[/edit] - -Обратите внимание: константы состояний перенесены в прототип, чтобы `AnimatingMenu` их тоже унаследовал. diff --git a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.view/index.html b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.view/index.html deleted file mode 100755 index 705919d8..00000000 --- a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.view/index.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.view/menu.js b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.view/menu.js deleted file mode 100755 index 9a91f30e..00000000 --- a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.view/menu.js +++ /dev/null @@ -1,28 +0,0 @@ -function Menu(state) { - this._state = state || this.STATE_CLOSED; -}; - -Menu.prototype.STATE_OPEN = 1; -Menu.prototype.STATE_CLOSED = 0; - -Menu.prototype.open = function() { - this._state = this.STATE_OPEN; -}; - -Menu.prototype.close = function() { - this._state = this.STATE_CLOSED; -}; - -Menu.prototype._stateAsString = function() { - switch (this._state) { - case this.STATE_OPEN: - return 'открыто'; - - case this.STATE_CLOSED: - return 'закрыто'; - } -}; - -Menu.prototype.showState = function() { - alert(this._stateAsString()); -} \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/source.view/index.html b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/source.view/index.html deleted file mode 100755 index 20de9758..00000000 --- a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/source.view/index.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/source.view/menu.js b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/source.view/menu.js deleted file mode 100755 index 996329ad..00000000 --- a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/source.view/menu.js +++ /dev/null @@ -1,28 +0,0 @@ -function Menu(state) { - this._state = state || Menu.STATE_CLOSED; -}; - -Menu.STATE_OPEN = 1; -Menu.STATE_CLOSED = 0; - -Menu.prototype.open = function() { - this._state = Menu.STATE_OPEN; -}; - -Menu.prototype.close = function() { - this._state = Menu.STATE_CLOSED; -}; - -Menu.prototype._stateAsString = function() { - switch (this._state) { - case Menu.STATE_OPEN: - return 'открыто'; - - case Menu.STATE_CLOSED: - return 'закрыто'; - } -}; - -Menu.prototype.showState = function() { - alert(this._stateAsString()); -}; \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/task.md b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/task.md deleted file mode 100644 index dd080260..00000000 --- a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Меню с таймером для анимации - -[importance 5] - -Есть класс `Menu`. У него может быть два состояния: открыто `STATE_OPEN` и закрыто `STATE_CLOSED`. - -Создайте наследника `AnimatingMenu`, который добавляет третье состояние `STATE_ANIMATING`. -
        -
      • При вызове `open()` состояние меняется на `STATE_ANIMATING`, а через 1 секунду, по таймеру, открытие завершается вызовом `open()` родителя.
      • -
      • Вызов `close()` при необходимости отменяет таймер анимации (назначаемый в `open`) и передаёт вызов родительскому `close`.
      • -
      • Метод `showState` для нового состояния выводит `"анимация"`, для остальных -- полагается на родителя.
      • -
      - -[edit src="source"]Исходный документ, вместе с тестом[/edit] \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/6-constructor-inherited/solution.md b/1-js/9-prototypes/5-class-inheritance/6-constructor-inherited/solution.md deleted file mode 100644 index 4339ffe9..00000000 --- a/1-js/9-prototypes/5-class-inheritance/6-constructor-inherited/solution.md +++ /dev/null @@ -1,26 +0,0 @@ -**Нет, не распознает, выведет `false`.** - -Свойство `constructor` содержится в `prototype` функции по умолчанию, интерпретатор не поддерживает его корректность. Посмотрим, чему оно равно и откуда оно будет взято в данном случае. - -Порядок поиска свойства `rabbit.constructor`, по цепочке прототипов: -
        -
      1. `rabbit` -- это пустой объект, в нём нет.
      2. -
      3. `Rabbit.prototype` -- в него при помощи `Object.create` записан пустой объект, наследующий от `Animal.prototype`. Поэтому `constructor'а` в нём также нет.
      4. -
      5. `Animal.prototype` -- у функции `Animal` свойство `prototype` никто не менял. Поэтому оно содержит `Animal.prototype.constructor == Animal`.
      6. -
      - -```js -//+ run -function Animal() {} - -function Rabbit() {} -Rabbit.prototype = Object.create(Animal.prototype); - -var rabbit = new Rabbit(); - -*!* -alert( rabbit.constructor == Rabbit ); // false -alert( rabbit.constructor == Animal ); // true -*/!* -``` - diff --git a/1-js/9-prototypes/5-class-inheritance/6-constructor-inherited/task.md b/1-js/9-prototypes/5-class-inheritance/6-constructor-inherited/task.md deleted file mode 100644 index ea7fde62..00000000 --- a/1-js/9-prototypes/5-class-inheritance/6-constructor-inherited/task.md +++ /dev/null @@ -1,19 +0,0 @@ -# Что содержит constructor? - -[importance 5] - -В коде ниже создаётся простейшая иерархия классов: `Animal -> Rabbit`. - -Что содержит свойство `rabbit.constructor`? Распознает ли проверка в `alert` объект как `Rabbit`? - -```js -function Animal() {} - -function Rabbit() {} -Rabbit.prototype = Object.create(Animal.prototype); - -var rabbit = new Rabbit(); - -alert( rabbit.constructor == Rabbit ); // что выведет? -``` - diff --git a/1-js/9-prototypes/5-class-inheritance/article.md b/1-js/9-prototypes/5-class-inheritance/article.md deleted file mode 100644 index 3aa7012c..00000000 --- a/1-js/9-prototypes/5-class-inheritance/article.md +++ /dev/null @@ -1,358 +0,0 @@ -# Наследование классов в JavaScript - -Наследование на уровне объектов в JavaScript, как мы видели, реализуется через ссылку `__proto__`. - -Теперь поговорим о наследовании на уровне классов, то есть когда объекты, создаваемые, к примеру, через `new Admin`, должны иметь все методы, которые есть у объектов, создаваемых через `new User`, и ещё какие-то свои. - -[cut] - -## Наследование Array от Object - -Для реализации наследования в наших классах мы будем использовать тот же подход, который принят внутри JavaScript. - -Взглянем на него ещё раз на примере `Array`, который наследует от `Object`: - - - -
        -
      • Методы массивов `Array` хранятся в `Array.prototype`.
      • -
      • `Array.prototype` имеет прототипом `Object.prototype`.
      • -
      - -Поэтому когда экземпляры класса `Array` хотят получить метод массива -- они берут его из своего прототипа, например `Array.prototype.slice`. - -Если же нужен метод объекта, например, `hasOwnProperty`, то его в `Array.prototype` нет, и он берётся из `Object.prototype`. - -Отличный способ "потрогать это руками" -- запустить в консоли команду `console.dir([1,2,3])`. - -Вывод в Chrome будет примерно таким: - - - -Здесь отчётливо видно, что сами данные и `length` находятся в массиве, дальше в `__proto__` идут методы для массивов `concat`, то есть `Array.prototype`, а далее -- `Object.prototype`. - -[smart header="`console.dir` для доступа к свойствам"] -Обратите внимание, я использовал именно `console.dir`, а не `console.log`, поскольку `log` зачастую выводит объект в виде строки, без доступа к свойствам. -[/smart] - -## Наследование в наших классах - -Применим тот же подход для наших классов: объявим класс `Rabbit`, который будет наследовать от `Animal`. - -Вначале создадим два этих класса по отдельности, они пока что будут совершенно независимы. - -`Animal`: - -```js -function Animal(name) { - this.name = name; - this.speed = 0; -} - -Animal.prototype.run = function(speed) { - this.speed += speed; - alert( this.name + ' бежит, скорость ' + this.speed ); -}; - -Animal.prototype.stop = function() { - this.speed = 0; - alert( this.name + ' стоит' ); -}; -``` - -`Rabbit`: - -```js -function Rabbit(name) { - this.name = name; - this.speed = 0; -} - -Rabbit.prototype.jump = function() { - this.speed++; - alert( this.name + ' прыгает' ); -}; - -var rabbit = new Rabbit('Кроль'); -``` - -Для того, чтобы наследование работало, объект `rabbit = new Rabbit` должен использовать свойства и методы из своего прототипа `Rabbit.prototype`, а если их там нет, то -- свойства и метода родителя, которые хранятся в `Animal.prototype`. - -Если ещё короче -- порядок поиска свойств и методов должен быть таким: `rabbit -> Rabbit.prototype -> Animal.prototype`, по аналогии с тем, как это сделано для объектов и массивов. - -Для этого можно поставить ссылку `__proto__` с `Rabbit.prototype` на `Animal.prototype`. - -Можно сделать это так: -```js -Rabbit.prototype.__proto__ = Animal.prototype; -``` - -Однако, прямой доступ к `__proto__` не поддерживается в IE10-, поэтому для поддержки этих браузеров мы используем функцию `Object.create`. Она либо встроена либо легко эмулируется во всех браузерах. - -Класс `Animal` остаётся без изменений, а `Rabbit.prototype` мы будем создавать с нужным прототипом, используя `Object.create`: - -```js -//+ no-beautify -function Rabbit(name) { - this.name = name; - this.speed = 0; -} - -*!* -// задаём наследование -Rabbit.prototype = Object.create(Animal.prototype); -*/!* - -// и добавим свой метод (или методы...) -Rabbit.prototype.jump = function() { ... }; -``` - -Теперь выглядеть иерархия будет так: - - - -В `prototype` по умолчанию всегда находится свойство `constructor`, указывающее на функцию-конструктор. В частности, `Rabbit.prototype.constructor == Rabbit`. Если мы рассчитываем использовать это свойство, то при замене `prototype` через `Object.create` нужно его явно сохранить: - -```js -Rabbit.prototype = Object.create(Animal.prototype); -Rabbit.prototype.constructor = Rabbit; -``` - -## Полный код наследования - -Для наглядности -- вот итоговый код с двумя классами `Animal` и `Rabbit`: - -```js -// 1. Конструктор Animal -function Animal(name) { - this.name = name; - this.speed = 0; -} - -// 1.1. Методы -- в прототип - -Animal.prototype.stop = function() { - this.speed = 0; - alert( this.name + ' стоит' ); -} - -Animal.prototype.run = function(speed) { - this.speed += speed; - alert( this.name + ' бежит, скорость ' + this.speed ); -}; - - -// 2. Конструктор Rabbit -function Rabbit(name) { - this.name = name; - this.speed = 0; -} - -// 2.1. Наследование -Rabbit.prototype = Object.create(Animal.prototype); -Rabbit.prototype.constructor = Rabbit; - -// 2.2. Методы Rabbit -Rabbit.prototype.jump = function() { - this.speed++; - alert( this.name + ' прыгает, скорость ' + this.speed ); -} -``` - -Как видно, наследование задаётся всего одной строчкой, поставленной в правильном месте. - -Обратим внимание: `Rabbit.prototype = Object.create(proto)` присваивается сразу после объявления конструктора, иначе он перезатрёт уже записанные в прототип методы. - -[warn header="Неправильный вариант: `Rabbit.prototype = new Animal`"] - -В некоторых устаревших руководствах предлагают вместо `Object.create(Animal.prototype)` записывать в прототип `new Animal`, вот так: - -```js -// вместо Rabbit.prototype = Object.create(Animal.prototype) -Rabbit.prototype = new Animal(); -``` - - -Частично, он рабочий, поскольку иерархия прототипов будет такая же, ведь `new Animal` -- это объект с прототипом `Animal.prototype`, как и `Object.create(Animal.prototype)`. Они в этом плане идентичны. - -Но у этого подхода важный недостаток. Как правило мы не хотим создавать `Animal`, а хотим только унаследовать его методы! - -Более того, на практике создание объекта может требовать обязательных аргументов, влиять на страницу в браузере, делать запросы к серверу и что-то ещё, чего мы хотели бы избежать. Поэтому рекомендуется использовать вариант с `Object.create`. -[/warn] - -## Вызов конструктора родителя - -Посмотрим внимательно на конструкторы `Animal` и `Rabbit` из примеров выше: - -```js -function Animal(name) { - this.name = name; - this.speed = 0; -} - -function Rabbit(name) { - this.name = name; - this.speed = 0; -} -``` - -Как видно, объект `Rabbit` не добавляет никакой особенной логики при создании, которой не было в `Animal`. - -Чтобы упростить поддержку кода, имеет смысл не дублировать код конструктора `Animal`, а напрямую вызвать его: - -```js -function Rabbit(name) { - Animal.apply(this, arguments); -} -``` - -Такой вызов запустит функцию `Animal` в контексте текущего объекта, со всеми аргументами, она выполнится и запишет в `this` всё, что нужно. - -Здесь можно было бы использовать и `Animal.call(this, name)`, но `apply` надёжнее, так как работает с любым количеством аргументов. - -## Переопределение метода - -Итак, `Rabbit` наследует `Animal`. Теперь если какого-то метода нет в `Rabbit.prototype` -- он будет взят из `Animal.prototype`. - -В `Rabbit` может понадобиться задать какие-то методы, которые у родителя уже есть. Например, кролики бегают не так, как остальные животные, поэтому переопределим метод `run()`: - -```js -Rabbit.prototype.run = function(speed) { - this.speed++; - this.jump(); -}; -``` - -Вызов `rabbit.run()` теперь будет брать `run` из своего прототипа: - - - - -### Вызов метода родителя внутри своего - -Более частая ситуация -- когда мы хотим не просто заменить метод на свой, а взять метод родителя и расширить его. Скажем, кролик бежит так же, как и другие звери, но время от времени подпрыгивает. - -Для вызова метода родителя можно обратиться к нему напрямую, взяв из прототипа: - -```js -//+ run - Rabbit.prototype.run = function() { -*!* - // вызвать метод родителя, передав ему текущие аргументы - Animal.prototype.run.apply(this, arguments); -*/!* - this.jump(); - } -``` - -Обратите внимание на вызов через `apply` и явное указание контекста. - -Если вызвать просто `Animal.prototype.run()`, то в качестве `this` функция `run` получит `Animal.prototype`, а это неверно, нужен текущий объект. - - -## Итого - -
        -
      • Для наследования нужно, чтобы "склад методов потомка" (`Child.prototype`) наследовал от "склада метода родителей" (`Parent.prototype`). - -Это можно сделать при помощи `Object.create`: - -Код: - -```js -Rabbit.prototype = Object.create(Animal.prototype); -``` - -
      • -
      • Для того, чтобы наследник создавался так же, как и родитель, он вызывает конструктор родителя в своём контексте, используя `apply(this, arguments)`, вот так: - -```js -function Rabbit(...) { - Animal.apply(this, arguments); -} -``` - -
      • -
      • При переопределении метода родителя в потомке, к исходному методу можно обратиться, взяв его напрямую из прототипа: - -```js -Rabbit.prototype.run = function() { - var result = Animal.prototype.run.apply(this, ...); - // result -- результат вызова метода родителя -} -``` - -
      • -
      - -Структура наследования полностью: - -```js -//+ run -*!* -// --------- Класс-Родитель ------------ -*/!* -// Конструктор родителя пишет свойства конкретного объекта -function Animal(name) { - this.name = name; - this.speed = 0; -} - -// Методы хранятся в прототипе -Animal.prototype.run = function() { - alert(this.name + " бежит!") -} - -*!* -// --------- Класс-потомок ----------- -*/!* -// Конструктор потомка -function Rabbit(name) { - Animal.apply(this, arguments); -} - -// Унаследовать -*!* -Rabbit.prototype = Object.create(Animal.prototype); -*/!* - -// Желательно и constructor сохранить -Rabbit.prototype.constructor = Rabbit; - -// Методы потомка -Rabbit.prototype.run = function() { - // Вызов метода родителя внутри своего - Animal.prototype.run.apply(this); - alert( this.name + " подпрыгивает!" ); -}; - -// Готово, можно создавать объекты -var rabbit = new Rabbit('Кроль'); -rabbit.run(); -``` - -Такое наследование лучше функционального стиля, так как не дублирует методы в каждом объекте. - -Кроме того, есть ещё неявное, но очень важное архитектурное отличие. - -Зачастую вызов конструктора имеет какие-то побочные эффекты, например влияет на документ. Если конструктор родителя имеет какое-то поведение, которое нужно переопределить в потомке, то в функциональном стиле это невозможно. - -Иначе говоря, в функциональном стиле в процессе создания `Rabbit` нужно обязательно вызывать `Animal.apply(this, arguments)`, чтобы получить методы родителя -- и если этот `Animal.apply` кроме добавления методов говорит: "Му-у-у!", то это проблема: - -```js -function Animal() { - this.walk = function() { - alert('walk') - }; - alert( 'Му-у-у!' ); -} - -function Rabbit() { - Animal.apply(this, arguments); // как избавиться от мычания, но получить walk? -} -``` - -...Которой нет в прототипном подходе, потому что в процессе создания `new Rabbit` мы вовсе не обязаны вызывать конструктор родителя. Ведь методы находятся в прототипе. - -Поэтому прототипный подход стоит предпочитать функциональному как более быстрый и универсальный. А что касается красоты синтаксиса -- она сильно лучше в новом стандарте ES6, которым можно пользоваться уже сейчас, если взять транслятор [babeljs](https://babeljs.io/). - diff --git a/1-js/9-prototypes/5-class-inheritance/class-inheritance-array-object.png b/1-js/9-prototypes/5-class-inheritance/class-inheritance-array-object.png deleted file mode 100644 index 9bb33ca2..00000000 Binary files a/1-js/9-prototypes/5-class-inheritance/class-inheritance-array-object.png and /dev/null differ diff --git a/1-js/9-prototypes/5-class-inheritance/class-inheritance-array-object@2x.png b/1-js/9-prototypes/5-class-inheritance/class-inheritance-array-object@2x.png deleted file mode 100644 index 975c24fc..00000000 Binary files a/1-js/9-prototypes/5-class-inheritance/class-inheritance-array-object@2x.png and /dev/null differ diff --git a/1-js/9-prototypes/5-class-inheritance/class-inheritance-rabbit-animal.png b/1-js/9-prototypes/5-class-inheritance/class-inheritance-rabbit-animal.png deleted file mode 100644 index e1a75f0e..00000000 Binary files a/1-js/9-prototypes/5-class-inheritance/class-inheritance-rabbit-animal.png and /dev/null differ diff --git a/1-js/9-prototypes/5-class-inheritance/class-inheritance-rabbit-animal@2x.png b/1-js/9-prototypes/5-class-inheritance/class-inheritance-rabbit-animal@2x.png deleted file mode 100644 index ad834718..00000000 Binary files a/1-js/9-prototypes/5-class-inheritance/class-inheritance-rabbit-animal@2x.png and /dev/null differ diff --git a/1-js/9-prototypes/5-class-inheritance/class-inheritance-rabbit-run-animal.png b/1-js/9-prototypes/5-class-inheritance/class-inheritance-rabbit-run-animal.png deleted file mode 100644 index 9582a793..00000000 Binary files a/1-js/9-prototypes/5-class-inheritance/class-inheritance-rabbit-run-animal.png and /dev/null differ diff --git a/1-js/9-prototypes/5-class-inheritance/class-inheritance-rabbit-run-animal@2x.png b/1-js/9-prototypes/5-class-inheritance/class-inheritance-rabbit-run-animal@2x.png deleted file mode 100644 index 3316df5a..00000000 Binary files a/1-js/9-prototypes/5-class-inheritance/class-inheritance-rabbit-run-animal@2x.png and /dev/null differ diff --git a/1-js/9-prototypes/5-class-inheritance/console_dir_array@2x.png b/1-js/9-prototypes/5-class-inheritance/console_dir_array@2x.png deleted file mode 100755 index cd8506f4..00000000 Binary files a/1-js/9-prototypes/5-class-inheritance/console_dir_array@2x.png and /dev/null differ diff --git a/1-js/9-prototypes/6-instanceof/1-strange-instanceof/solution.md b/1-js/9-prototypes/6-instanceof/1-strange-instanceof/solution.md deleted file mode 100644 index 6bf55d43..00000000 --- a/1-js/9-prototypes/6-instanceof/1-strange-instanceof/solution.md +++ /dev/null @@ -1,7 +0,0 @@ -Да, это выглядит достаточно странно, поскольку объект `a` не создавался функцией `B`. - -Но методу `instanceof` на самом деле вообще не важна функция. Он смотрит на её `prototype` и сверяет его с цепочкой `__proto__` объекта. - -В данном случае `a.__proto__ == B.prototype`, поэтому `instanceof` возвращает `true`. - -По логике `instanceof` именно прототип задаёт "тип объекта", поэтому `instanceof` работает именно так. \ No newline at end of file diff --git a/1-js/9-prototypes/6-instanceof/1-strange-instanceof/task.md b/1-js/9-prototypes/6-instanceof/1-strange-instanceof/task.md deleted file mode 100644 index a3c73f74..00000000 --- a/1-js/9-prototypes/6-instanceof/1-strange-instanceof/task.md +++ /dev/null @@ -1,21 +0,0 @@ -# Странное поведение instanceof - -[importance 5] - -Почему `instanceof` в коде ниже возвращает `true`, ведь объект `a` явно создан не `B()`? - -```js -//+ run -function A() {} - -function B() {} - -A.prototype = B.prototype = {}; - -var a = new A(); - -*!* -alert( a instanceof B ); // true -*/!* -``` - diff --git a/1-js/9-prototypes/6-instanceof/2-instanceof-result/solution.md b/1-js/9-prototypes/6-instanceof/2-instanceof-result/solution.md deleted file mode 100644 index 646e3aa1..00000000 --- a/1-js/9-prototypes/6-instanceof/2-instanceof-result/solution.md +++ /dev/null @@ -1,18 +0,0 @@ -Да, распознает. - -Он проверяет наследование с учётом цепочки прототипов. - -```js -//+ run -function Animal() {} - -function Rabbit() {} -Rabbit.prototype = Object.create(Animal.prototype); - -var rabbit = new Rabbit(); - -alert( rabbit instanceof Rabbit ); // true -alert( rabbit instanceof Animal ); // true -alert( rabbit instanceof Object ); // true -``` - diff --git a/1-js/9-prototypes/6-instanceof/2-instanceof-result/task.md b/1-js/9-prototypes/6-instanceof/2-instanceof-result/task.md deleted file mode 100644 index 56c971e4..00000000 --- a/1-js/9-prototypes/6-instanceof/2-instanceof-result/task.md +++ /dev/null @@ -1,23 +0,0 @@ -# Что выведет instanceof? - -[importance 5] - -В коде ниже создаётся простейшая иерархия классов: `Animal -> Rabbit`. - -Что выведет [instanceof](/instanceof)? - -Распознает ли он `rabbit` как `Animal`, `Rabbit` и к тому же `Object`? - -```js -function Animal() {} - -function Rabbit() {} -Rabbit.prototype = Object.create(Animal.prototype); - -var rabbit = new Rabbit(); - -alert( rabbit instanceof Rabbit ); -alert( rabbit instanceof Animal ); -alert( rabbit instanceof Object ); -``` - diff --git a/1-js/9-prototypes/6-instanceof/article.md b/1-js/9-prototypes/6-instanceof/article.md deleted file mode 100644 index 174333cf..00000000 --- a/1-js/9-prototypes/6-instanceof/article.md +++ /dev/null @@ -1,91 +0,0 @@ -# Проверка класса: "instanceof" - -Оператор `instanceof` позволяет проверить, какому классу принадлежит объект, с учетом прототипного наследования. - -[cut] - -## Алгоритм работы instanceof [#ref-instanceof] - -Вызов `obj instanceof Constructor` возвращает `true`, если объект принадлежит классу `Constructor` или его родителям. - -Пример использования: - -```js -//+ run -function Rabbit() {} - -*!* -// создаём объект -*/!* -var rabbit = new Rabbit(); - -// проверяем -- этот объект создан Rabbit? -*!* -alert( rabbit instanceof Rabbit ); // true, верно -*/!* -``` - -Массив `arr` принадлежит классу `Array`, но также и является объектом `Object`. Это верно, так как массивы наследуют от объектов: - -```js -//+ run -var arr = []; -alert( arr instanceof Array ); // true -alert( arr instanceof Object ); // true -``` - -Как это часто бывает в JavaScript, здесь есть ряд тонкостей. В некоторых ситуациях, проверка может даже ошибаться! - -**Алгоритм проверки `obj instanceof Constructor`:** - -
        -
      1. Получить `obj.__proto__`
      2. -
      3. Сравнить `obj.__proto__` с `Constructor.prototype`
      4. -
      5. Если не совпадает, тогда заменить `obj` на `obj.__proto__` и повторить проверку на шаге 2 до тех пор, пока либо не найдется совпадение (результат `true`), либо цепочка прототипов не закончится (результат `false`).
      6. -
      - -В проверке `rabbit instanceof Rabbit` совпадение происходит на первом же шаге этого алгоритма, так как: `rabbit.__proto__ == Rabbit.prototype`. - -А если рассмотреть `arr instanceof Object`, то совпадение будет найдено на следующем шаге, так как `arr.__proto__.__proto__ == Object.prototype`. - -Забавно, что сама функция-констуктор не участвует в процессе проверки! Важна только цепочка прототипов для проверяемого объекта. - -Это может приводить к забавному результату и даже ошибкам в проверке при изменении `prototype`, например: - -```js -//+ run -// Создаём объект rabbit, как обычно -function Rabbit() {} -var rabbit = new Rabbit(); - -// изменили prototype... -Rabbit.prototype = {}; - -// ...instanceof перестал работать! -*!* -alert( rabbit instanceof Rabbit ); // false -*/!* -``` - -Стоит ли говорить, что это один из доводов для того, чтобы никогда не менять `prototype`? Так сказать, во избежание. - -[warn header="Не друзья: `instanceof` и фреймы"] - -Оператор `instanceof` не срабатывает, когда значение приходит из другого окна или фрейма. - -Например, массив, который создан в ифрейме и передан родительскому окну -- будет массивом *в том ифрейме*, но не в родительском окне. Проверка `instanceof Array` в родительском окне вернёт `false`. - -Вообще, у каждого окна и фрейма -- своя иерархия объектов и свой `window` . - -Как правило, эта проблема возникает со встроенными объектами, в этом случае используется проверка внутреннего свойства `[[Class]]`, которое подробнее описано в главе [](/class-instanceof). -[/warn] - - -## Итого - -
        -
      • Оператор `obj instanceof Func` проверяет тот факт, что `obj` является результатом вызова `new Func`. Он учитывает цепочку `__proto__`, поэтому наследование поддерживается.
      • -
      • Оператор `instanceof` не сможет проверить тип значения, если объект создан в одном окне/фрейме, а проверяется в другом. Это потому, что в каждом окне -- своя иерархия объектов. Для точной проверки типов встроенных объектов можно использовать свойство `[[Class]]`.
      • -
      - -Оператор `instanceof` особенно востребован в случаях, когда мы работаем с иерархиями классов. Это наилучший способ проверить принадлежность тому или иному классу с учётом наследования. diff --git a/1-js/9-prototypes/7-oop-errors/1-format-error/solution.md b/1-js/9-prototypes/7-oop-errors/1-format-error/solution.md deleted file mode 100644 index 1532bd3c..00000000 --- a/1-js/9-prototypes/7-oop-errors/1-format-error/solution.md +++ /dev/null @@ -1,28 +0,0 @@ -```js -//+ run -function FormatError(message) { - this.name = "FormatError"; - - this.message = message; - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, this.constructor); - } else { - this.stack = (new Error()).stack; - } - -} - -FormatError.prototype = Object.create(SyntaxError.prototype); -FormatError.prototype.constructor = FormatError; - -// Использование - -var err = new FormatError("ошибка форматирования"); - -alert( err.message ); // ошибка форматирования -alert( err.name ); // FormatError -alert( err.stack ); // стек на момент генерации ошибки - -alert( err instanceof SyntaxError ); // true -``` \ No newline at end of file diff --git a/1-js/9-prototypes/7-oop-errors/1-format-error/task.md b/1-js/9-prototypes/7-oop-errors/1-format-error/task.md deleted file mode 100644 index bd78ad35..00000000 --- a/1-js/9-prototypes/7-oop-errors/1-format-error/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# Унаследуйте от SyntaxError - -[importance 5] - -Создайте ошибку `FormatError`, которая будет наследовать от встроенного класса `SyntaxError`. - -Синтаксис для её создания -- такой же, как обычно: - -```js -var err = new FormatError("ошибка форматирования"); - -alert( err.message ); // ошибка форматирования -alert( err.name ); // FormatError -alert( err.stack ); // стек на момент генерации ошибки - -alert( err instanceof SyntaxError ); // true -``` diff --git a/1-js/9-prototypes/7-oop-errors/article.md b/1-js/9-prototypes/7-oop-errors/article.md deleted file mode 100644 index 13fd9715..00000000 --- a/1-js/9-prototypes/7-oop-errors/article.md +++ /dev/null @@ -1,285 +0,0 @@ -# Свои ошибки, наследование от Error - -Когда мы работаем с внешними данными, возможны самые разные ошибки. - -Если приложение сложное, то ошибки естественным образом укладываются в иерархию, разобраться в которой помогает `instanceof`. - -## Свой объект ошибки - -Для примера создадим функцию `readUser(json)`, которая будет разбирать JSON с данными посетителя. Мы его получаем с сервера -- может, нашего, а может -- чужого, в общем -- желательно проверить на ошибки. А может, это даже и не JSON, а какие-то другие данные -- не важно, для наглядности поработаем с JSON. - -Пример `json` на входе в функцию: `{ "name": "Вася", "age": 30 }`. - -В процессе работы `readUser` возможны различные ошибки. Одна -- очевидно, `SyntaxError` -- если передан некорректный JSON. - -Но могут быть и другие, например `PropertyError` -- эта ошибка будет возникать, если в прочитанном объекте нет свойства `name` или `age`. - -Реализуем класс `PropertyError`: - -```js -function PropertyError(property) { - Error.call(this, property) ; - this.name = "PropertyError"; - - this.property = property; - this.message = "Ошибка в свойстве " + property; - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, PropertyError); - } else { - this.stack = (new Error()).stack; - } - -} - -PropertyError.prototype = Object.create(Error.prototype); -``` - -В этом коде вы можете видеть ряд важных деталей, важных именно для ошибок: - -
      -
      `name` -- имя ошибки.
      -
      Должно совпадать с именем функции.
      -
      `message` -- сообщение об ошибке.
      -
      Несмотря на то, что `PropertyError` наследует от `Error` (последняя строка), конструктор у неё немного другой. Он принимает не сообщение об ошибке, а название свойства `property`, ну а сообщение генерируется из него. - -В результате в объекте ошибки есть как стандартное свойство `message`, так и более точное `property`. - -Это частая практика -- добавлять в объект ошибки свойства, которых нет в базовых объектах `Error`, более подробно описывающие ситуацию для данного класса ошибок.
      -
      `stack` -- стек вызовов, которые в итоге привели к ошибке.
      -
      У встроенных объектов `Error` это свойство есть автоматически, вот к примеру: -```js -//+ run -function f() { - alert( new Error().stack ); -} - -f(); // выведет список вложенных вызовов, с номерами строк, где они были сделаны -``` - -Если же объект ошибки делаем мы, то "по умолчанию" у него такого свойства у него не будет. Нам нужно как-то самим узнавать последовательность вложенных вызовов на текущий момент. Однако удобного способа сделать это в JavaScript нет, поэтому мы поступаем хитро и копируем его из нового объекта `new Error`, который генерируем тут же. - -В V8 (Chrome, Opera, Node.JS) есть нестандартное расширение [Error.captureStackTrace](https://code.google.com/p/v8-wiki/wiki/JavaScriptStackTraceApi), которое позволяет стек получать. - -Это делает строка из кода выше: -```js -Error.captureStackTrace(this, PropertyError); -``` - -Такой вызов записывает в объект `this` (текущий объект ошибки) стек вызовов, ну а второй аргумент -- вообще не обязателен, но если есть, то говорит, что при генерации стека нужно на этой функции остановиться. В результате в стеке будет информация о цепочке вложенных вызовов вплоть до вызова `PropertyError`. - -То есть, будет последовательность вызовов до генерации ошибки, но не включая код самого конструктора ошибки, который, как правило, не интересен. Такое поведение максимально соответствует встроенным ошибкам JavaScript. -
      -
      - -[smart header="Конструктор родителя здесь не обязателен"] -Обычно, когда мы наследуем, то вызываем конструктор родителя. В данном случае вызов выглядит как `Error.call(this, message)`. - -Строго говоря, этот вызов здесь не обязателен. Встроенный конструктор `Error` ничего полезного не делает, даже свойство `this.message` (не говоря уже об `name` и `stack`) не назначает. Единственный возможный смысл его вызова -- он ставит специальное внутреннее свойство `[[ErrorData]]`, которое выводится в `toString` и позволяет увидить, что это ошибка. Поэтому по стандарту вызывать конструктор `Error` при наследовании в таких случаях рекомендовано. -[/smart] - - -## instanceof + try..catch = ♡ - -Давайте теперь используем наш новый класс для `readUser`: - -```js -//+ run -*!* -// Объявление -*/!* -function PropertyError(property) { - this.name = "PropertyError"; - - this.property = property; - this.message = "Ошибка в свойстве " + property; - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, PropertyError); - } else { - this.stack = (new Error()).stack; - } - -} - -PropertyError.prototype = Object.create(Error.prototype); - -*!* -// Генерация ошибки -*/!* -function readUser(data) { - - var user = JSON.parse(data); - - if (!user.age) { - throw new PropertyError("age"); - } - - if (!user.name) { - throw new PropertyError("name"); - } - - return user; -} - -*!* -// Запуск и try..catch -*/!* - -try { - var user = readUser('{ "age": 25 }'); -} catch (err) { - if (err instanceof PropertyError) { - if (err.property == 'name') { - // если в данном месте кода возможны анонимы, то всё нормально -*!* - alert( "Здравствуйте, Аноним!" ); -*/!* - } else { - alert( err.message ); // Ошибка в свойстве ... - } - } else if (err instanceof SyntaxError) { - alert( "Ошибка в синтаксисе данных: " + err.message ); - } else { - throw err; // неизвестная ошибка, не знаю что с ней делать - } -} -``` - -Всё работает -- и наша ошибка `PropertyError` и встроенная `SyntaxError` корректно генерируются, перехватываются, обрабатываются. - -Обратим внимание на проверку типа ошибки в `try..catch`. Оператор `instanceof` проверяет класс с учётом наследования. Это значит, что если мы в дальнейшем решим создать новый тип ошибки, наследующий от `PropertyError`, то проверка `err instanceof PropertyError` для класса-наследника тоже будет работать. Код получился расширяемым, это очень важно. - -## Дальнейшее наследование - -`PropertyError` -- это просто общего вида ошибка в свойстве. Создадим ошибку `PropertyRequiredError`, которая означает, что свойства нет. - -Эт подвид `PropertyError`, так что унаследуем он неё. Общий вид конструктора-наследника -- стандартный: - -```js -function PropertyRequiredError(property) { - // вызываем конструктор родителя и передаём текущие аргументы - PropertyError.apply(this, arguments); - ... -} -``` - -Достаточно ли в наследнике просто вызвать конструктор родителя? Увы, нет. - -Если так поступить, то свойство `this.name` будет некорректным, да и `Error.captureStackTrace` тоже получит неправильную функцию вторым параметром. - -Можно ли как-то поправить конструктор родителя, чтобы от него было проще наследовать? - -Для этого нужно убрать из него упоминания о конкретном классе `PropertyError`, чтобы сделать код универсальным. Частично -- это возможно. Как мы помним, существует свойство `constructor`, которое есть в `prototype` по умолчанию, и которое мы можем намеренно сохранить при наследовании. - -Исправим родителя `PropertyError` для более удобного наследования от него: - -```js -function PropertyError(property) { - this.name = "PropertyError"; - - this.property = property; - this.message = "Ошибка в свойстве " + property; - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, *!*this.constructor*/!*); // (*) - } else { - this.stack = (new Error()).stack; - } - -} - -PropertyError.prototype = Object.create(Error.prototype); -*!* -PropertyError.prototype.constructor = PropertyError; -*/!* -``` - -В строке `(*)` вместо ссылки на `PropertyError` используем `constructor` чтобы получить именно конструктор для текущего объекта. В наследнике там будет `PropertyRequiredError`, как и задумано. - -Мы убрали одну жёсткую привязку к `PropertyError`, но со второй (`this.name`), увы, сложности. Оно должно содержать имя ошибки, то есть, имя её функции-конструктора. Его можно получить через `this.name = this.constructor.name`, но в IE11- это работать не будет. - -Если подерживать IE11-, то тут уж придётся в наследнике его записывать вручную. - -Полный код для наследника: - -```js -function PropertyRequiredError(property) { - PropertyError.apply(this, arguments); - this.name = 'PropertyRequiredError'; - this.message = 'Отсутствует свойство ' + property; -} - -PropertyRequiredError.prototype = Object.create(PropertyError.prototype); -PropertyRequiredError.prototype.constructor = PropertyRequiredError; - -var err = new PropertyRequiredError("age"); -// пройдёт проверку -alert( err instanceof PropertyError ); // true -``` - -Здесь заодно и `message` в наследнике было перезаписано на более точное. Если хочется избежать записи и перезаписи, то можно оформить его в виде геттера через `Object.defineProperty`. - -## Итого - -
        -
      • Чтобы наследовать от ошибок `Error`, нужно самостоятельно позаботиться о `name`, `message` и `stack`.
      • -
      • Благодаря тому, что `instanceof` поддерживает наследование, удобно организуются проверки на нужный тип. В иерархию ошибок можно в любой момент добавить новые классы, с понятным кодом и предсказуемым поведением.
      • -
      - -Чтобы создавать наследники от `Error` было проще, можно создать класс `CustomError`, записать в него универсальный код, наподобие `PropertyError` и далее наследовать уже от него: - -```js -*!* -// общего вида "наша" ошибка -*/!* -function CustomError(message) { - this.name = "CustomError"; - this.message = message; - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, this.constructor); - } else { - this.stack = (new Error()).stack; - } - -} - -CustomError.prototype = Object.create(Error.prototype); -CustomError.prototype.constructor = CustomError; - -*!* -// наследник -*/!* -function PropertyError(property) { - CustomError.call(this, "Отсутствует свойство " + property) - this.name = "PropertyError"; - - this.property = property; -} - -PropertyError.prototype = Object.create(CustomError.prototype); -PropertyError.prototype.constructor = PropertyError; - -*!* -// и ещё уровень -*/!* -function PropertyRequiredError(property) { - PropertyError.call(this, property); - this.name = 'PropertyRequiredError'; - this.message = 'Отсутствует свойство ' + property; -} - -PropertyRequiredError.prototype = Object.create(PropertyError.prototype); -PropertyRequiredError.prototype.constructor = PropertyRequiredError; - -*!* -// использование -*/!* -var err = new PropertyRequiredError("age"); -// пройдёт проверку -alert( err instanceof PropertyRequiredError ); // true -alert( err instanceof PropertyError ); // true -alert( err isntanceof CustomError ); // true -alert( err isntanceof Error ); // true -``` diff --git a/1-js/9-prototypes/8-mixins/article.md b/1-js/9-prototypes/8-mixins/article.md deleted file mode 100644 index a87d1bf3..00000000 --- a/1-js/9-prototypes/8-mixins/article.md +++ /dev/null @@ -1,170 +0,0 @@ -# Примеси - -В JavaScript невозможно унаследовать от двух и более объектов. Ссылка `__proto__` -- только одна. - -Но потребность такая существует -- к примеру, мы написали код, релизующий методы работы с шаблонизатором или методы по обмену событиями, и хочется легко и непринуждённо добавлять эти возможности к любому классу. - -Обычно это делают через примеси. - -Примесь (англ. mixin) -- класс или объект, реализующий какое-либо чётко выделенное поведение. Используется для уточнения поведения других классов, не предназначен для самостоятельного использования. - - - -## Пример примеси - -Самый простой вариант примеси -- это объект с полезными методами, которые мы просто копируем в нужный прототип. - -Например: - -```js -//+ run -*!* -// примесь -*/!* -var sayHiMixin = { - sayHi: function() { - alert("Привет " + this.name); - }, - sayBye: function() { - alert("Пока " + this.name); - } -}; - -*!* -// использование: -*/!* -function User(name) { - this.name = name; -} - -// передать методы примеси -for(var key in sayHiMixin) User.prototype[key] = sayHiMixin[key]; - -// User "умеет" sayHi -new User("Вася").sayHi(); // Привет Вася -``` - -Как видно из примера, методы примеси активно используют `this` и предназначены именно для запуска в контексте "объекта-носителя примеси". - -Если какие-то из методов примеси не нужны -- их можно перезаписать своими после копирования. - - -## Примесь для событий - -Теперь пример из реальной жизни. - -Важный аспект, который может понадобиться объектам -- это умение работать с событиями. - -То есть, чтобы объект мог специальным вызовом генерировать "уведомление о событии", а на эти уведомления другие объекты могли "подписываться", чтобы их получать. - -Например, объект "Пользователь" при входе на сайт может генерировать событие `"login"`, а другие объекты, например "Календарь" может такие уведомления получать и подгружать информацию о пользователе. - -Или объект "Меню" может при выборе пункта меню генерировать событие `"select"` с информацией о выбранном пункте меню, а другие объекты -- подписавшись на это событие, будут узнавать об этом. - -События -- это средство "поделиться информацией" с неопределённым кругом заинтересованных лиц. А там уже кому надо -- тот среагирует. - -Примесь `eventMixin`, реализующая события: - -```js -var eventMixin = { - - /** - * Подписка на событие - * Использование: - * menu.on('select', function(item) { ... } - */ - on: function(eventName, handler) { - if (!this._eventHandlers) this._eventHandlers = {}; - if (!this._eventHandlers[eventName]) { - this._eventHandlers[eventName] = []; - } - this._eventHandlers[eventName].push(handler); - }, - - /** - * Прекращение подписки - * menu.off('select', handler) - */ - off: function(eventName, handler) { - var handlers = this._eventHandlers && this._eventHandlers[eventName]; - if (!handlers) return; - for(var i=0; i -
    8. `.on(имя события, функция)` -- назначает функцию к выполнению при наступлении события с данным именем. Такие функции хранятся в защищённом свойстве объекта `_eventHandlers`.
    9. -
    10. `.off(имя события, функция)` -- удаляет функцию из списка предназначенных к выполнению.
    11. -
    12. `.trigger(имя события, аргументы)` -- генерирует событие, при этом вызываются все назначенные на него функции, и им передаются аргументы.
    13. -
    - -Использование: - -```js -// Класс Menu с примесью eventMixin -function Menu() { - // ... -} - -for(var key in eventMixin) { - Menu.prototype[key] = eventMixin[key]; -} - -// Генерирует событие select при выборе значения -Menu.prototype.choose = function(value) { -*!* - this.trigger("select", value); -*/!* -} - -// Создадим меню -var menu = new Menu(); - -// При наступлении события select вызвать эту функцию -*!* -menu.on("select", function(value) { - alert("Выбрано значение " + value); -}); -*/!* - -// Запускаем выбор (событие select вызовет обработчики) -menu.choose("123"); -``` - -...То есть, смысл событий -- обычно в том, что объект, в процессе своей деятельности, внутри себя (`this.trigger`) генерирует уведомления, на которые внешний код через `menu.on(...)` может быть подписан. И узнавать из них ценную информцию о происходящем, например -- что выбран некий пункт меню. - -Один раз написав методы `on/off/trigger` в примеси, мы затем можем использовать их во множестве прототипов. - -## Итого - -
      -
    • Примесь -- объект, содержащий методы и свойства для реализации конкретного функционала. -Возможны вариации этого приёма проектирования. Например, примесь может предусматривать конструктор, который должен запускаться в конструкторе объекта. Но как правило просто набора методов хватает.
    • -
    • Для добавления примеси в класс -- её просто "подмешивают" в прототип.
    • -
    • "Подмешать" можно сколько угодно примесей, но если имена методов в разных примесях совпадают, то возможны конфликты. Их уже разрешать -- разработчику. Например, можно заменить конфликтующий метод на свой, который будет решать несколько задач сразу. Конфликты при грамотно оформленных примесях возникают редко.
    - - diff --git a/1-js/9-prototypes/index.md b/1-js/9-prototypes/index.md deleted file mode 100644 index 3b67318e..00000000 --- a/1-js/9-prototypes/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# ООП в прототипном стиле - -В этом разделе мы изучим прототипы и классы на них -- де-факто стандарт объектно-ориентированной разработки в JavaScript. \ No newline at end of file diff --git a/1-js/index.md b/1-js/index.md index f3fadc3a..c313cb85 100644 --- a/1-js/index.md +++ b/1-js/index.md @@ -1,5 +1,6 @@ -# Язык JavaScript +# The JavaScript language -Эта часть позволит вам изучить JavaScript с нуля или упорядочить и дополнить существующие знания. +Here we learn JavaScript, starting from scratch and go on to advanced concepts like OOP. + +We concentrate on the language itself here, with the minimum of environment-specific notes. -Мы будем использовать браузер в качестве окружения, но основное внимание будет уделяться именно самому языку JavaScript. \ No newline at end of file diff --git a/1-js/plan3.txt b/1-js/plan3.txt new file mode 100644 index 00000000..4d553174 --- /dev/null +++ b/1-js/plan3.txt @@ -0,0 +1,5 @@ +todo: + +intl? +proxy? +eval? diff --git a/10-regular-expressions-javascript/1-regexp-introduction/article.md b/10-regular-expressions-javascript/1-regexp-introduction/article.md deleted file mode 100644 index 3fb0195a..00000000 --- a/10-regular-expressions-javascript/1-regexp-introduction/article.md +++ /dev/null @@ -1,104 +0,0 @@ -# Паттерны и флаги - -Регулярные выражения –- мощное средство поиска и замены в строке. - -В JavaScript регулярные выражения реализованы отдельным объектом `RegExp` и интегрированы в методы строк. -[cut] - -## Регэкспы - -Регулярное выражение (оно же "регэксп", "регулярка" или просто "рег"), состоит из *паттерна* (он же "шаблон") и необязательных *флагов*. - -Синтаксис создания регулярного выражения: - -```js -var regexp = new RegExp("шаблон", "флаги"); -``` - -Как правило, используют более короткую запись: шаблон внутри слешей `"/"`: - -```js -var regexp = /шаблон/; // без флагов -var regexp = /шаблон/gmi; // с флагами gmi (изучим их дальше) -``` - -Слэши `"/"` говорят JavaScript о том, что это регулярное выражение. Они играют здесь ту же роль, что и кавычки для обозначения строк. - -## Использование - -Основа регулярного выражения -- паттерн. Это строка, которую можно расширить специальными символами, которые делают поиск намного мощнее. - -В простейшем случае, если флагов и специальных символов нет, поиск по паттерну -- то же самое, что и обычный поиск подстроки: - -```js -//+ run -var str = "Я люблю JavaScript!"; // будем искать в этой строке - -var regexp = /лю/; -alert( str.search(regexp) ); // 2 -``` - -Сравните с обычным поиском: - -```js -//+ run -var str = "Я люблю JavaScript!"; - -var substr = "лю"; -alert( str.indexOf(substr) ); // 2 -``` - -Как видим, то же самое, разве что для регэкспа использован метод [search](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/search) -- он как раз работает с регулярными выражениями, а для подстроки -- [indexOf](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf). - -Но это соответствие лишь кажущееся. Очень скоро мы усложним регулярные выражения, и тогда увидим, что они гораздо мощнее. - -[smart header="Цветовые обозначения"] -Здесь и далее в тексте используется следующая цветовая схема: -
      -
    • регэксп (регулярное выражение) - красный
    • -
    • строка - синий
    • -
    • результат - зеленый
    • -
    -[/smart] - -## Флаги - -Регулярные выражения могут иметь флаги, которые влияют на поиск. - -В JavaScript их всего три: - -
    -
    `i`
    -
    Если этот флаг есть, то регэксп ищет независимо от регистра, то есть не различает между `А` и `а`.
    -
    `g`
    -
    Если этот флаг есть, то регэксп ищет все совпадения, иначе -- только первое.
    -
    `m`
    -
    Многострочный режим.
    -
    - -Самый простой для понимания из этих флагов -- безусловно, `i`. - -Пример его использования: - -```js -//+ run -var str = "Я люблю JavaScript!"; // будем искать в этой строке - -alert( str.search( *!*/ЛЮ/*/!* ) ); // -1 -alert( str.search( *!*/ЛЮ/i*/!* ) ); // 2 -``` - -
      -
    1. С регом /ЛЮ/ вызов вернул `-1`, что означает "не найдено" (как и в `indexOf`),
    2. -
    3. С регом /ЛЮ/i вызов нашёл совпадение на позиции 2, так как стоит флаг `i`, а значит "лю" тоже подходит.
    4. -
    - -Другие флаги мы рассмотрим в последующих главах. - -## Итого - -
      -
    • Регулярное выражение состоит из шаблона и необязательных флагов `g`, `i` и `m`.
    • -
    • Поиск по регулярному выражению без флагов и спец. символов, которые мы изучим далее -- это то же самое, что и обычный поиск подстроки в строке. Но флаги и спец. символы, как мы увидим далее, могут сделать его гораздо мощнее.
    • -
    • Метод строки `str.search(regexp)` возвращает индекс, на котором найдено совпадение.
    • -
    diff --git a/10-regular-expressions-javascript/10-regexp-ahchors/1-start-end/solution.md b/10-regular-expressions-javascript/10-regexp-ahchors/1-start-end/solution.md deleted file mode 100644 index e5118b47..00000000 --- a/10-regular-expressions-javascript/10-regexp-ahchors/1-start-end/solution.md +++ /dev/null @@ -1,6 +0,0 @@ - -Нам нужна строка, которая начинается -- и тут же кончается. То есть, пустая. - -Или, если быть ближе к механике регэкспов, то движок сначала будет искать в тексте начальную позицию ``pattern`^`, а как только найдёт её -- будет ожидать конечной ``pattern`$`. - -Заметим, что и ``pattern`^` и ``pattern`$` не требуют наличия символов. Это -- проверки. В пустой строке движок сначала проверит первую, а потом -- вторую -- и зафиксирует совпадение. \ No newline at end of file diff --git a/10-regular-expressions-javascript/10-regexp-ahchors/1-start-end/task.md b/10-regular-expressions-javascript/10-regexp-ahchors/1-start-end/task.md deleted file mode 100644 index f5df587e..00000000 --- a/10-regular-expressions-javascript/10-regexp-ahchors/1-start-end/task.md +++ /dev/null @@ -1,4 +0,0 @@ -# Регэксп ^$ - -Предложите строку, которая подойдёт под регулярное выражение ``pattern`^$`. - diff --git a/10-regular-expressions-javascript/10-regexp-ahchors/2-test-mac/solution.md b/10-regular-expressions-javascript/10-regexp-ahchors/2-test-mac/solution.md deleted file mode 100644 index 5a91a896..00000000 --- a/10-regular-expressions-javascript/10-regexp-ahchors/2-test-mac/solution.md +++ /dev/null @@ -1,21 +0,0 @@ -Двузначное шестнадцатиричное число -- это ``pattern`[0-9a-f]{2}` (с учётом флага ``pattern`/i`). - -Нам нужно одно такое число, и за ним ещё 5 с двоеточиями перед ними: ``pattern`[0-9a-f]{2}(:[0-9a-f]{2}){5}` - -И, наконец, совпадение должно начинаться в начале строки и заканчиваться -- в конце. То есть, строка целиком должна подходить под шаблон. Для этого обернём шаблон в ``pattern`^...$`. - -Итого, в действии: - - -```js -//+ run -var re = /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/i; - -alert( re.test('01:32:54:67:89:AB') ); // true - -alert( re.test('0132546789AB') ); // false (нет двоеточий) - -alert( re.test('01:32:54:67:89') ); // false (5 чисел, а не 6) - -alert( re.test('01:32:54:67:89:ZZ') ) // false (ZZ в конце) -``` diff --git a/10-regular-expressions-javascript/10-regexp-ahchors/2-test-mac/task.md b/10-regular-expressions-javascript/10-regexp-ahchors/2-test-mac/task.md deleted file mode 100644 index fecf7dd0..00000000 --- a/10-regular-expressions-javascript/10-regexp-ahchors/2-test-mac/task.md +++ /dev/null @@ -1,20 +0,0 @@ -# Проверьте MAC-адрес - -MAC-адрес сетевого интерфейса состоит из шести двузначиных шестандцатиричных чисел, разделённых двоеточием. - -Например: ``subject`'01:32:54:67:89:AB'`. - -Напишите регулярное выражение, которое по строке проверяет, является ли она корректным MAC-адресом. - -Использование: -```js -var re = ваш регэксп - -alert( re.test('01:32:54:67:89:AB') ); // true - -alert( re.test('0132546789AB') ); // false (нет двоеточий) - -alert( re.test('01:32:54:67:89') ); // false (5 чисел, а не 6) - -alert( re.test('01:32:54:67:89:ZZ') ) // false (ZZ в конце) -``` diff --git a/10-regular-expressions-javascript/10-regexp-ahchors/article.md b/10-regular-expressions-javascript/10-regexp-ahchors/article.md deleted file mode 100644 index 16abf01c..00000000 --- a/10-regular-expressions-javascript/10-regexp-ahchors/article.md +++ /dev/null @@ -1,68 +0,0 @@ -# Начало строки ^ и конец $ - -Знак каретки '^' и доллара '$' имеют в регулярном выражении особый смысл. Их называют "якорями" (anchor - англ.). -[cut] - -Каретка ^ совпадает в начале текста, а доллар $ -- в конце. - -**Якоря являются не символами, а проверками.** - -До этого мы говорили о регулярных выражениях, которые ищут один или несколько символов. Если совпадение есть -- эти символы включаются в результат. - -А якоря -- не такие. Когда поиск ходит до якоря -- он проверяет, есть ли соответствие, если есть -- продолжает идти по шаблону, не прибавляя ничего к результату. - -Каретку ^ обычно используют, чтобы указать, что регулярное выражение необходимо проверить именно с начала текста. - -Например, без каретки найдёт все числа: - -```js -//+ run -var str = '100500 попугаев съели 500100 бананов!'; -alert( str.match(/\d+/ig) ); // 100500, 500100 (нашло все числа) -``` - -А с кареткой -- только первое: - -```js -//+ run -var str = '100500 попугаев съели 500100 бананов!'; -alert( str.match(/^\d+/ig) ); // 100500 (только в начале строки)*!* -``` - -Знак доллара $ используют, чтобы указать, что паттерн должен заканчиваться в конце текста. - -Аналогичный пример с долларом для поиска числа в конце: - -```js -//+ run -var str = '100500 попугаев съели 500100'; -alert( str.match(/\d+$/ig) ); // 500100 -``` - -Оба якоря используют одновременно, если требуется, чтобы шаблон охватывал текст с начала и до конца. Обычно это требуется при валидации. - -Например, мы хотим проверить, что в переменной `num` хранится именно десятичная дробь. - -Ей соответствует регэксп \d+\.\d+. Но простой поиск найдёт дробь в любом тексте: - -```js -//+ run -var num = "ля-ля 12.34"; -alert( num.match(/\d+\.\d+/ig) ); // 12.34 -``` - -Наша же задача -- проверить, что `num` *целиком* соответствует паттерну \d+\.\d+. - -Для этого обернём шаблон в якоря ^...$: - -```js -//+ run -var num = "ля-ля 12.34"; -alert( num.match(/^\d+\.\d+$/ig) ); // null, не дробь - -var num = "12.34"; -alert( num.match(/^\d+\.\d+$/ig) ); // 12.34, дробь! -``` - -Теперь поиск ищет начало текста, за которым идёт число, затем точка, ещё число и конец текста. Это как раз то, что нужно. - diff --git a/10-regular-expressions-javascript/11-regexp-multiline-mode/article.md b/10-regular-expressions-javascript/11-regexp-multiline-mode/article.md deleted file mode 100644 index 918a85eb..00000000 --- a/10-regular-expressions-javascript/11-regexp-multiline-mode/article.md +++ /dev/null @@ -1,89 +0,0 @@ -# Многострочный режим, флаг "m" - -Многострочный режим включается, если у регэкспа есть флаг /m. -[cut] - -В этом случае изменяется поведение ^ и $. - -В многострочном режиме якоря означают не только начало/конец текста, но и начало/конец строки. - -## Начало строки ^ - -В примере ниже текст состоит из нескольких строк. Паттерн /^\d+/gm берёт число с начала каждой строки: - -```js -//+ run -var str = '1е место: Винни\n' + - '2е место: Пятачок\n' + - '33е место: Слонопотам'; - -*!* -alert( str.match(/^\d+/gm) ); // 1, 2, 33 -*/!* -``` - -Обратим внимание -- без флага /m было бы найдено только первое число: - -```js -//+ run -var str = '1е место: Винни\n' + - '2е место: Пятачок\n' + - '33е место: Слонопотам'; - -alert( str.match(/^\d+/g) ); // 1 -``` - -Это потому что в обычном режиме каретка ^ -- это только начало текста, а в многострочном -- начало любой строки. - -Движок регулярных выражений двигается по тексту, и как только видит начало строки, начинает искать там \d+. - -## Конец строки $ - -Символ доллара $ ведёт себя аналогично. - -Регулярное выражение [а-я]+$ в следующем примере находит последнее слово в каждой строке: - -```js -//+ run -var str = '1е место: Винни\n' + - '2е место: Пятачок\n' + - '33е место: Слонопотам'; - -alert( str.match(/[а-я]+$/gim) ); // Винни,Пятачок,Слонопотам -``` - -Без флага m якорь $ обозначал бы конец всего текста, и было бы найдено только последнее слово. - -[smart header="Якорь `$` против `\n`"] -Для того, чтобы найти конец строки, можно использовать не только `$`, но и символ `\n`. - -Но, в отличие от `$`, символ `\n` во-первых берёт символ в результат, а во-вторых -- не совпадает в конце текста (если, конечно, последний символ -- не конец строки). - -Посмотрим, что будет с примером выше, если вместо [а-я]+$ использовать [а-я]+\n: - -```js -//+ run -var str = '1е место: Винни\n' + - '2е место: Пятачок\n' + - '33е место: Слонопотам'; - -alert( str.match(/[а-я]+\n/gim) ); -/* -Винни -,Пятачок -*/ -``` - -Всего два результата: Винни\n (с символом перевода строки) и Пятачок\n. Последнее слово "Слонопотам" здесь не даёт совпадения, так как после него нет перевода строки. -[/smart] - -## Итого - -В мультистрочном режиме: -
      -
    • Символ `^` означает начало строки.
    • -
    • Символ `$` означает конец строки.
    • -
    - -Оба символа являются проверками, они не добавляют ничего к результату. Про них также говорят, что "они имеют нулевую длину". - diff --git a/10-regular-expressions-javascript/12-regexp-lookahead/article.md b/10-regular-expressions-javascript/12-regexp-lookahead/article.md deleted file mode 100644 index 2cd4438c..00000000 --- a/10-regular-expressions-javascript/12-regexp-lookahead/article.md +++ /dev/null @@ -1,4 +0,0 @@ -# Предпросмотр (неготово) - -Требуется добавить главу про предпросмотр lookahead. - diff --git a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/article.md b/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/article.md deleted file mode 100644 index 582c1066..00000000 --- a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/article.md +++ /dev/null @@ -1,305 +0,0 @@ -# Чёрная дыра бэктрекинга - -Некоторые регулярные выражения, с виду являясь простыми, могут выполняться оооочень долго, и даже "подвешивать" интерпретатор JavaScript. - -Рано или поздно, с этим сталкивается любой разработчик, потому что нечаянно создать такое регулярное выражение -- легче лёгкого. - -Типична ситуация, когда регулярное выражение до поры до времени работает нормально, и вдруг на каком-то тексте как начнёт "подвешивать" интерпретатор и есть 100% процессора. - -Это может стать уязвимостью. Например, если JavaScript выполняется на сервере, то при разборе данных, присланных посетителем, он может зависнуть, если использует подобный регэксп. На клиенте тоже возможно подобное, при использовании регэкспа для подсветки синтаксиса. - -Такие уязвимости "убивали" почтовые сервера и системы обмена сообщениями и до появления JavaScript, и наверно будут "убивать" и после его исчезновения. Так что мы просто обязаны с ними разобраться. - -[cut] - -## Пример - -План изложения у нас будет таким: - -
      -
    1. Сначала посмотрим на проблему в реальной ситуации.
    2. -
    3. Потом упростим реальную ситуацию до "корней" и увидим, откуда она берётся.
    4. -
    - -Рассмотрим, например, поиск по HTML. - -Мы хотим найти теги с атрибутами, то есть совпадения вида <a href="..." class=doc ...>. - -Самый простой способ это сделать -- <[^>]*>. Но он же и не совсем корректный, так как тег может выглядеть так: <a test="<>" href="#">. То есть, внутри "закавыченного" атрибута может быть символ `>`. Простейший регэксп на нём остановится и найдёт <a test="<>. - -Соответствие: -``` -<[^>]*....> - -``` - -А нам нужен весь тег. - -Для того, чтобы правильно обрабатывать такие ситуации, нужно учесть их в регулярном выражении. Оно будет иметь вид <тег (ключ=значение)*>. - -Если перевести на язык регэкспов, то: <\w+(\s*\w+=(\w+|"[^"]*")\s*)*>: -
      -
    1. <\w+ -- начало тега
    2. -
    3. (\s*\w+=(\w+|"[^"]*")\s*)* -- произвольное количество пар вида `слово=значение`, где "значение" может быть также словом \w+, либо строкой в кавычках "[^"]*".
    4. -
    - - -Мы пока не учитываем все детали грамматики HTML, ведь строки возможны и в 'одинарных' кавычках, но на данный момент этого достаточно. Главное, что регулярное выражение получилось в меру простым и понятным. - - -Испытаем полученный регэксп в действии: - -```js -//+ run -var reg = /<\w+(\s*\w+=(\w+|"[^"]*")\s*)*>/g; - -var str='...
    ... ...'; - -alert( str.match(reg) ); // , -``` - -Отлично, всё работает! Нашло как длинный тег <a test="<>" href="#">, так и одинокий <b>. - -А теперь -- демонстрация проблемы. - -Если запустить пример ниже, то он может подвесить браузер: - -```js -//+ run -var reg = /<\w+(\s*\w+=(\w+|"[^"]*")\s*)*>/g; - -var str = "/g; - -var str = "(\d+)*$. - -В большинстве движков регэкспов, например в Chrome или IE, этот поиск выполняется очень долго (осторожно, может "подвесить" браузер): - -```js -//+ run -alert( '12345678901234567890123456789123456789z'.match(/(\d+)*$/) ); -``` - -В чём же дело, что не так с регэкспом? - -Внимательный читатель, посмотрев на него, наверняка удивится, ведь он "какой-то странный". Квантификатор * здесь выглядит лишним. - -Если хочется найти число, то с тем же успехом можно искать \d+$. - -Да, этот регэксп носит искусственный характер, но, разобравшись с ним, мы поймём и практический пример, данный выше. Причина их медленной работы одинакова. - -В целом, с регэкспом "всё так", синтаксис вполне допустимый. Проблема в том, как выполняется поиск по нему. - -Посмотрим, что происходит при поиске в строке 123456789z: - -
      -
    1. Первым делом, движок регэкспов пытается найти \d+. Плюс + является жадным по умолчанию, так что он хватает все цифры, какие может: - -``` -\d+....... -(123456789)z -``` -
    2. -
    3. Затем движок пытается применить звёздочку вокруг скобок (\d+)*, но больше цифр нет, так что звёздочка не даёт повторений. - -Затем в шаблоне идёт символ конца строки $, а в тексте -- символ z. - -``` - X -\d+........$ -(123456789)z -``` -Соответствия нет. -
    4. -
    5. Так как соответствие не найдено, то "жадный" плюс + отступает на один символ (бэктрекинг). - -Теперь `\d+` -- это все цифры, за исключением последней: -``` -\d+....... -(12345678)9z -``` -
    6. -
    7. После бэктрекинга, \d+ содержит всё число, кроме последней цифры. Движок снова пытается найти совпадение, уже с новой позиции (`9`). - -Звёздочка (\d+)* теперь может быть применена -- она даёт число 9: - -``` - -\d+.......\d+ -(12345678)(9)z -``` -Движок пытается найти `$`, но это ему не удаётся -- на его пути опять `z`: - -``` - X -\d+.......\d+ -(12345678)(9)z -``` - -Так как совпадения нет, то поисковой движок отступает назад ещё раз. -
    8. -
    9. Теперь первое число \d+ будет содержать 7 цифр, а остаток строки 89 становится вторым \d+: - - -``` - X -\d+......\d+ -(1234567)(89)z -``` - -Увы, всё ещё нет соответствия для $. - -Поисковой движок снова должен отступить назад. При этом последний жадный квантификатор отпускает символ. В данном случае это означает, что укорачивается второй \d+, до одного символа 8, и звёздочка забирает следующий 9. - - -``` - X -\d+......\d+\d+ -(1234567)(8)(9)z -``` -
    10. -
    11. ...И снова неудача. Второе и третье \d+ отступили по-максимуму, так что сокращается снова первое число, до 123456, а звёздочка берёт оставшееся: - -``` - X -\d+.......\d+ -(123456)(789)z -``` - -Снова нет совпадения. Процесс повторяется, последний жадный квантификатор + отпускает один символ (`9`): - -``` - X -\d+.....\d+ \d+ -(123456)(78)(9)z -``` -
    12. -
    13. -...И так далее. -
    14. -
    - -Получается, что движок регулярных выражений перебирает все комбинации из `123456789` и их подпоследовательности. А таких комбинаций очень много. - -На этом месте умный читатель может воскликнуть: "Во всём виноват бэктрекинг? Давайте включим ленивый режим -- и не будет никакого бэктрекинга!" - -Что ж, заменим \d+ на \d+? и посмотрим (аккуратно, может подвесить браузер): - -```js -//+ run -alert( '12345678901234567890123456789123456789z'.match(/(\d+?)*$/) ); -``` - -Не помогло! - -**Ленивые регулярные выражения делают то же самое, но в обратном порядке.** - -Просто подумайте о том, как будет в этом случае работать поисковой движок. - -Некоторые движки регулярных выражений содержат хитрые проверки и конечные автоматы, которые позволяют избежать бесконечного перебора или кардинально ускорить его, но все движки и не всегда. - -Возвращаясь к примеру выше -- при поиске <(\s*\w+=\w+\s*)*> в строке <a=b a=b a=b a=b происходит то же самое. - -Поиск успешно начинается, выбирается некая комбинация из \s*\w+=\w+\s*, которая, так как в конце нет `>`, оказывается не подходящей. Движок честно отступает, пробует другую комбинацию -- и так далее. - -## Что делать? - -Проблема -- в сверхмноговариантном переборе. - -Движок регулярных выражений перебирает кучу возможных вариантов скобок там, где это не нужно. - -Например, в регэкспе (\d+)*$ нам (людям) очевидно, что в (\d+) откатываться не нужно. От того, что вместо одного \d+ у нас два независимых \d+\d+, ничего не изменится. - -Без разницы: - -``` -\d+........ -(123456789)z - -\d+...\d+.... -(1234)(56789)z -``` - -Если вернуться к более реальному примеру <(\s*\w+=\w+\s*)*> то -cам алгоритм поиска, который у нас в голове, предусматривает, что мы "просто" ищем тег, а потом пары `атрибут=значение` (сколько получится). - -Никакого "отката" здесь не нужно. - -В современных регулярных выражениях для решения этой проблемы придумали "possessive" (сверхжадные? неоткатные? точный перевод пока не устоялся) квантификаторы, которые вообще не используют бэктрегинг. - -То есть, они даже проще, чем "жадные" -- берут максимальное количество символов и всё. Поиск продолжается дальше. При несовпадении никакого возврата не происходит. - -Это, c стороны уменьшает количество возможных результатов, но с другой стороны -- в ряде случаев очевидно, что возврат (уменьшение количество повторений квантификатора) результата не даст. А только потратит время, что как раз и доставляет проблемы. Как раз такие ситуации и описаны выше. - -Есть и другое средство -- "атомарные скобочные группы", которые запрещают перебор внутри скобок, по сути позволяя добиваться того же, что и сверхжадные квантификаторы, - -К сожалению, в JavaScript они не поддерживаются. - -Однако, можно получить подобный эффект при помощи предпросмотра. Подробное описание соответствия с учётом синтаксиса сверхжадных квантификаторов и атомарных групп есть в статьях [Regex: Emulate Atomic Grouping (and Possessive Quantifiers) with LookAhead](http://instanceof.me/post/52245507631/regex-emulate-atomic-grouping-with-lookahead) и [Mimicking Atomic Groups](http://blog.stevenlevithan.com/archives/mimic-atomic-groups), здесь же мы останемся в рамках синтаксиса JavaScript. - -Взятие максимального количества повторений `a+` без отката выглядит так: (?=(a+))\1. - -То есть, иными словами, предпросмотр ?= ищет максимальное количество повторений a+, доступных с текущей позиции. А затем они "берутся в результат" обратной ссылкой \1. Дальнейший поиск -- после найденных повторений. - -Откат в этой логике принципе не предусмотрен, поскольку предпросмотр "откатываться" не умеет. То есть, если предпросмотр нашёл 5 штук a+, и в результате поиск не удался, то он не будет откатываться на 4 повторения. Эта возможность в предпросмотре отсутствует, а в данном случае она как раз и не нужна. - -Исправим регэксп для поиска тега с атрибутами <\w+(\s*\w+=(\w+|"[^"]*")\s*)*>, описанный в начале главы. Используем предпросмотр, чтобы запретить откат на меньшее количество пар `атрибут=значение`: - -```js -//+ run -// регэксп для пары атрибут=значение -var attr = /(\s*\w+=(\w+|"[^"]*")\s*)/ - -// используем его внутри регэкспа для тега -var reg = new RegExp('<\\w+(?=(' + attr.source + '*))\\1>', 'g'); - -var good = '...
    ... ...'; - -var bad = ", -alert( bad.match(reg) ); // null (нет результатов, быстро) -``` - -Отлично, всё работает! Нашло как длинный тег <a test="<>" href="#">, так и одинокий <b>. - - - - - - - diff --git a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy1.png b/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy1.png deleted file mode 100644 index 8f207c4d..00000000 Binary files a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy1.png and /dev/null differ diff --git a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy11.png b/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy11.png deleted file mode 100644 index 713532ae..00000000 Binary files a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy11.png and /dev/null differ diff --git a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy2.png b/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy2.png deleted file mode 100644 index 0de64148..00000000 Binary files a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy2.png and /dev/null differ diff --git a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy3.png b/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy3.png deleted file mode 100644 index 8d0cd522..00000000 Binary files a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy3.png and /dev/null differ diff --git a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy4.png b/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy4.png deleted file mode 100644 index c693a42e..00000000 Binary files a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy4.png and /dev/null differ diff --git a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy5.png b/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy5.png deleted file mode 100644 index 63f0e9e4..00000000 Binary files a/10-regular-expressions-javascript/13-regexp-infinite-backtracking-problem/bad_backtrack_greedy5.png and /dev/null differ diff --git a/10-regular-expressions-javascript/2-regexp-methods/article.md b/10-regular-expressions-javascript/2-regexp-methods/article.md deleted file mode 100644 index fdf66de8..00000000 --- a/10-regular-expressions-javascript/2-regexp-methods/article.md +++ /dev/null @@ -1,392 +0,0 @@ -# Методы RegExp и String - -Регулярные выражения в JavaScript являются объектами класса [RegExp](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/RegExp). - -Кроме того, методы для поиска по регулярным выражениям встроены прямо в обычные строки `String`. - -К сожалению, общая структура встроенных методов слегка запутана, поэтому мы сначала рассмотрим их по отдельности, а затем -- рецепты по решению стандартных задач с ними. - -[cut] - -## str.search(reg) - -Этот метод мы уже видели. - -Он возвращает позицию первого совпадения или `-1`, если ничего не найдено. - -```js -//+ run -var str = "Люблю регэкспы я, но странною любовью"; - -alert( str.search( *!*/лю/i*/!* ) ); // 0 -``` - -**Ограничение метода `search` -- он всегда ищет только первое совпадение.** - -Нельзя заставить `search` искать дальше первого совпадения, такой синтаксис попросту не предусмотрен. Но есть другие методы, которые это умеют. - -## str.match(reg) без флага g - -Метод `str.match` работает по-разному, в зависимости от наличия или отсутствия флага `g`, поэтому сначала мы разберём вариант, когда его нет. - -В этом случае `str.match(reg)` находит только одно, первое совпадение. - -Результат вызова -- это массив, состоящий из этого совпадения, с дополнительными свойствами `index` -- позиция, на которой оно обнаружено и `input` -- строка, в которой был поиск. - -Например: - -```js -//+ run -var str = "ОЙ-Ой-ой"; - -var result = str.match( *!*/ой/i*/!* ); - -alert( result[0] ); // ОЙ (совпадение) -alert( result.index ); // 0 (позиция) -alert( result.input ); // ОЙ-Ой-ой (вся поисковая строка) -``` - -У этого массива не всегда только один элемент. - -**Если часть шаблона обозначена скобками, то она станет отдельным элементом массива.** - -Например: - -```js -//+ run -var str = "javascript - это такой язык"; - -var result = str.match( *!*/JAVA(SCRIPT)/i*/!* ); - -alert( result[0] ); // javascript (всё совпадение полностью) -alert( result[1] ); // script (часть совпадения, соответствующая скобкам) -alert( result.index ); // 0 -alert( result.input ); // javascript - это такой язык -``` - -Благодаря флагу `i` поиск не обращает внимание на регистр буквы, поэтому находит javascript. При этом часть строки, соответствующая SCRIPT, выделена в отдельный элемент массива. - -Позже мы ещё вернёмся к скобочным выражениям, они особенно удобны для поиска с заменой. - -## str.match(reg) с флагом g - -При наличии флага `g`, вызов `match` возвращает обычный массив из всех совпадений. - -Никаких дополнительных свойств у массива в этом случае нет, скобки дополнительных элементов не порождают. - -Например: - -```js -//+ run -var str = "ОЙ-Ой-ой"; - -var result = str.match( *!*/ой/ig*/!* ); - -alert( result ); // ОЙ, Ой, ой -``` - -Пример со скобками: - -```js -//+ run -var str = "javascript - это такой язык"; - -var result = str.match( *!*/JAVA(SCRIPT)/gi*/!* ); - -alert( result[0] ); // javascript -alert( result.length ); // 1 -alert( result.index ); // undefined -``` - -Из последнего примера видно, что элемент в массиве ровно один, и свойства `index` также нет. Такова особенность глобального поиска при помощи `match` -- он просто возвращает все совпадения. - -Для расширенного глобального поиска, который позволит получить все позиции и, при желании, скобки, нужно использовать метод [RegExp#exec](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec), которые будет рассмотрен далее. - -[warn header="В случае, если совпадений не было, `match` возвращает `null`"] -Обратите внимание, это важно -- если `match` не нашёл совпадений, он возвращает не пустой массив, а именно `null`. - -Это важно иметь в виду, чтобы не попасть в такую ловушку: - -```js -//+ run -var str = "Ой-йой-йой"; - -// результат match не всегда массив! -alert(str.match(/лю/gi).length) // ошибка! нет свойства length у null -``` -[/warn] - -## str.split(reg|substr, limit) - -Разбивает строку в массив по разделителю -- регулярному выражению `regexp` или подстроке `substr`. - -Обычно мы используем метод `split` со строками, вот так: - -```js -//+ run -alert('12-34-56'.split('-')) // [12, 34, 56] -``` - -Можно передать в него и регулярное выражение, тогда он разобьёт строку по всем совпадениям. - -Тот же пример с регэкспом: - -```js -//+ run -alert('12-34-56'.split(/-/)) // [12, 34, 56] -``` - -## str.replace(reg, str|func) - -Швейцарский нож для работы со строками, поиска и замены любого уровня сложности. - -Его простейшее применение -- поиск и замена подстроки в строке, вот так: - -```js -//+ run -// заменить дефис на двоеточие -alert('12-34-56'.replace("-", ":")) // 12:34-56 -``` - -**При вызове со строкой замены `replace` всегда заменяет только первое совпадение.** - -Чтобы заменить *все* совпадения, нужно использовать для поиска не строку `"-"`, а регулярное выражение /-/g, причём обязательно с флагом `g`: - -```js -//+ run -// заменить дефис на двоеточие -alert( '12-34-56'.replace( *!*/-/g*/!*, ":" ) ) // 12:34:56 -``` - - -В строке для замены можно использовать специальные символы: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    СпецсимволыДействие в строке замены
    `$$`Вставляет `"$"`.
    `$&`Вставляет всё найденное совпадение.
    $`Вставляет часть строки до совпадения.
    - $' - Вставляет часть строки после совпадения.
    - $*n* - где `n` -- цифра или двузначное число, обозначает `n-ю` по счёту скобку, если считать слева-направо.
    - -Пример использования скобок и `$1`, `$2`: - -```js -//+ run -var str = "Василий Пупкин"; - -alert(str.replace(/(Василий) (Пупкин)/, '$2, $1')) // Пупкин, Василий -``` - -Ещё пример, с использованием `$&`: - -```js -//+ run -var str = "Василий Пупкин"; - -alert(str.replace(/Василий Пупкин/, 'Великий $&!')) // Великий Василий Пупкин! -``` - -**Для ситуаций, который требуют максимально "умной" замены, в качестве второго аргумента предусмотрена функция.** - -Она будет вызвана для каждого совпадения, и её результат будет вставлен как замена. - -Например: - -```js -//+ run -var i = 0; - -// заменить каждое вхождение "ой" на результат вызова функции -alert("ОЙ-Ой-ой".replace(/ой/gi, function() { - return ++i; -})); // 1-2-3 -``` - -В примере выше функция просто возвращала числа по очереди, но обычно она основывается на поисковых данных. - -Эта функция получает следующие аргументы: - -
      -
    1. `str` -- найденное совпадение,
    2. -
    3. `p1, p2, ..., pn` -- содержимое скобок (если есть),
    4. -
    5. `offset` -- позиция, на которой найдено совпадение,
    6. -
    7. `s` -- исходная строка.
    8. -
    - -Если скобок в регулярном выражении нет, то у функции всегда будет ровно 3 аргумента: `replacer(str, offset, s)`. - -Используем это, чтобы вывести полную информацию о совпадениях: - -```js -//+ run -// вывести и заменить все совпадения -function replacer(str, offset, s) { - alert( "Найдено: " + str + " на позиции: " + offset + " в строке: " + s ); - return str.toLowerCase(); -} - -var result = "ОЙ-Ой-ой".replace(/ой/gi, replacer); -alert( 'Результат: ' + result ); // Результат: ой-ой-ой -``` - -С двумя скобочными выражениями -- аргументов уже 5: - -```js -//+ run -function replacer(str, name, surname, offset, s) { - return surname + ", " + name; -} - -alert(str.replace(/(Василий) (Пупкин)/, replacer)) // Пупкин, Василий -``` - -Функция -- это самый мощный инструмент для замены, какой только может быть. Она владеет всей информацией о совпадении и имеет доступ к замыканию, поэтому может всё. - -## regexp.test(str) - -Теперь переходим к методам класса `RegExp`. - -Метод `test` проверяет, есть ли хоть одно совпадение в строке `str`. Возвращает `true/false`. - -Работает, по сути, так же, как и проверка `str.search(reg) != -1`, например: - -```js -//+ run -var str = "Люблю регэкспы я, но странною любовью"; - -// эти две проверки идентичны -alert( *!*/лю/i*/!*.test(str) ) // true -alert( str.search(*!*/лю/i*/!*) != -1 ) // true -``` - -Пример с отрицательным результатом: - -```js -//+ run -var str = "Ой, цветёт калина..."; - -alert( *!*/javascript/i*/!*.test(str) ) // false -alert( str.search(*!*/javascript/i*/!*) != -1 ) // false -``` - -## regexp.exec(str) - -Для поиска мы уже видели методы: -
      -
    • `search` -- ищет индекс
    • -
    • `match` -- если регэксп без флага `g` -- ищет совпадение с подрезультатами в скобках
    • -
    • `match` -- если регэксп с флагом `g` -- ищет все совпадения, но без скобочных групп.
    • -
    - -Метод `regexp.exec` дополняет их. Он позволяет искать и все совпадения и скобочные группы в них. - -Он ведёт себя по-разному, в зависимости от того, есть ли у регэкспа флаг `g`. - -
      -
    • Если флага `g` нет, то `regexp.exec(str)` ищет и возвращает первое совпадение, является полным аналогом вызова `str.match(reg)`.
    • -
    • Если флаг `g` есть, то вызов `regexp.exec` возвращает первое совпадение и *запоминает* его позицию в свойстве `regexp.lastIndex`. Последующий поиск он начнёт уже с этой позиции. Если совпадений не найдено, то сбрасывает `regexp.lastIndex` в ноль.
    • -
    - -Это используют для поиска всех совпадений в цикле: - -```js -//+ run -var str = 'Многое по JavaScript можно найти на сайте http://javascript.ru'; - -var regexp = /javascript/ig; - -alert( "Начальное значение lastIndex: " + regexp.lastIndex ); - -while (result = regexp.exec(str)) { - alert( 'Найдено: ' + result[0] + ' на позиции:' + result.index ); - alert( 'Свойство lastIndex: ' + regexp.lastIndex ); -} - -alert( 'Конечное значение lastIndex: ' + regexp.lastIndex ); -``` - -Здесь цикл продолжается до тех пор, пока `regexp.exec` не вернёт `null`, что означает "совпадений больше нет". - -Найденные результаты последовательно помещаются в `result`, причём находятся там в том же формате, что и `match` -- с учётом скобок, со свойствами `result.index` и `result.input`. - -[smart header="Поиск с нужной позиции"] -Можно заставить `regexp.exec` искать сразу с нужной позиции, если поставить `lastIndex` вручную: - -```js -//+ run -var str = 'Многое по JavaScript можно найти на сайте http://javascript.ru'; - -var regexp = /javascript/ig; -regexp.lastIndex = 40; - -alert( regexp.exec(str).index ); // 49, поиск начат с 40й позиции -``` -[/smart] - -## Итого, рецепты - -Методы становятся гораздо понятнее, если разбить их использование по задачам, которые нужны в реальной жизни. - -
    -
    Для поиска только одного совпадения:
    -
    -
      -
    • Найти позицию первого совпадения -- `str.search(reg)`.
    • -
    • Найти само совпадение -- `str.match(reg)`.
    • -
    • Проверить, есть ли хоть одно совпадение -- `regexp.test(str)` или `str.search(reg) != -1`.
    • -
    • Найти совпадение с нужной позиции -- `regexp.exec(str)`, начальную позицию поиска задать в `regexp.lastIndex`.
    • -
    -
    -
    Для поиска всех совпадений:
    -
    -
      -
    • Найти массив совпадений -- `str.match(reg)`, с флагом `g`.
    • -
    • Получить все совпадения, с подробной информацией о каждом -- `regexp.exec(str)` с флагом `g`, в цикле.
    • -
    -
    - -
    Для поиска-и-замены:
    -
    -
      -
    • Замена на другую строку или функцией -- `str.replace(reg, str|func)`
    • -
    -
    -
    Для разбивки строки на части:
    -
    -
      -
    • `str.split(str|reg)`
    • -
    -
    -
    - -Зная эти методы, мы уже можем использовать регулярные выражения. - -Конечно, для этого желательно хорошо понимать их синтаксис и возможности, так что переходим к ним дальше. diff --git a/10-regular-expressions-javascript/3-regexp-character-classes/1-find-time-hh-mm/solution.md b/10-regular-expressions-javascript/3-regexp-character-classes/1-find-time-hh-mm/solution.md deleted file mode 100644 index 1fd6b26a..00000000 --- a/10-regular-expressions-javascript/3-regexp-character-classes/1-find-time-hh-mm/solution.md +++ /dev/null @@ -1,8 +0,0 @@ - -Ответ: \d\d:\d\d. - -```js -//+ run -alert( "Завтрак в 09:00.".match( /\d\d:\d\d/ ) ); // 09:00 -``` - diff --git a/10-regular-expressions-javascript/3-regexp-character-classes/1-find-time-hh-mm/task.md b/10-regular-expressions-javascript/3-regexp-character-classes/1-find-time-hh-mm/task.md deleted file mode 100644 index f92b52a5..00000000 --- a/10-regular-expressions-javascript/3-regexp-character-classes/1-find-time-hh-mm/task.md +++ /dev/null @@ -1,8 +0,0 @@ -# Найдите время - -Время имеет формат `часы:минуты`. И часы и минуты состоят из двух цифр, например: `09:00`. - -Напишите регулярное выражение для поиска времени в строке: Завтрак в 09:00. - -P.S. В этой задаче выражению позволительно найти и некорректное время, например `25:99`. - diff --git a/10-regular-expressions-javascript/3-regexp-character-classes/article.md b/10-regular-expressions-javascript/3-regexp-character-classes/article.md deleted file mode 100644 index 26fd9277..00000000 --- a/10-regular-expressions-javascript/3-regexp-character-classes/article.md +++ /dev/null @@ -1,271 +0,0 @@ -# Классы и спецсимволы - -Рассмотрим практическую задачу -- есть телефонный номер `"+7(903)-123-45-67"`, и нам нужно найти в этой строке цифры. А остальные символы нас не интересуют. - -Для поиска символов определённого вида в регулярных выражениях предусмотрены "классы символов". - -[cut] - -Класс символов -- это специальное обозначение, под которое подходит любой символ из определённого набора. - -Например, есть класс "любая цифра". Он обозначается `\d`. Это обозначение вставляется в шаблон, и при поиске под него подходит любая цифра. - -То есть, регулярное выражение /\d/ ищет ровно одну цифру: - -```js -//+ run -var str = "+7(903)-123-45-67"; - -var reg = /\d/; - -// не глобальный регэксп, поэтому ищет только первую цифру -alert( str.match(reg) ); // 7 -``` - -...Ну а для поиска всех цифр достаточно добавить к регэкспу флаг `g`: - -```js -//+ run -var str = "+7(903)-123-45-67"; - -var reg = /\d/g; - -alert( str.match(reg) ); // массив всех совпадений: 7,9,0,3,1,2,3,4,5,6,7 -``` - -## Важнейшие классы: \d \s \w - -Это был класс для цифр. - -Конечно же, есть и другие. - -Наиболее часто используются: -
    -
    `\d` (от английского "digit" -- "цифра")
    -
    Цифра, символ от `0` до `9`.
    -
    `\s` (от английского "space" -- "пробел")
    -
    Пробельный символ, включая табы, переводы строки и т.п.
    -
    `\w` (от английского "word" -- "слово")
    -
    Символ "слова", а точнее -- буква латинского алфавита или цифра или подчёркивание `'_'`. Не-английские буквы не являются `\w`, то есть русская буква не подходит.
    -
    - -Например, \d\s\w обозначает цифру, за которой идёт пробельный символ, а затем символ слова. - -Регулярное выражение может содержать одновременно и обычные символы и классы. - -Например, CSS\d найдёт строку CSS, с любой цифрой после неё: - -```js -//+ run -var str = "Стандарт CSS4 - это здорово"; -var reg = /CSS\d/ - -alert( str.match(reg) ); // CSS4 -``` - -И много классов подряд: - -```js -//+ run -alert( "Я люблю HTML5!".match(/\s\w\w\w\w\d/) ); // 'HTML5' -``` - -Совпадение (каждому классу в регэкспе соответствует один символ результата): - - - -## Граница слова \b - -Граница слова \b -- это особый класс. - -Он интересен тем, что обозначает не символ, а границу между символами. - -Например, \bJava\b найдёт слово Java в строке Hello, Java!, но не в строке Hello, Javascript!. - - -```js -//+ run - -alert( "Hello, Java!".match(/\bJava\b/) ); // Java -alert( "Hello, Javascript!".match(/\bJava\b/) ); // null -``` - -Граница имеет "нулевую ширину" в том смысле, что обычно символам регулярного выражения соответствуют символы строки, но не в этом случае. - -Граница -- это проверка. - -При поиске движок регулярных выражений идёт по шаблону и одновременно по строке, пытаясь построить соответствие. Когда он видит \b, то проверяет, что текущая позиция в строке подходит под одно из условий: -
      -
    • Начало текста, если первый символ `\w`.
    • -
    • Конец текста, если последний символ `\w`.
    • -
    • Внутри текста, если с одной стороны `\w`, а с другой -- не `\w`.
    • -
    - -Например, в строке Hello, Java! под `\b` подходят следующие позиции: - - - -Как правило, `\b` используется, чтобы искать отдельно стоящее слово. Не на русском конечно, хотя подобную проверку, как мы увидим далее, можно легко сделать для любого языка. А вот на английском, как в примере выше или для чисел, которые являются частным случаем `\w` -- легко. - -Например, регэксп \b\d\d\b ищет отдельно двузначные числа. Иными словами, он требует, чтобы до и после \d\d был символ, отличный от `\w` (или начало/конец текста). - - -## Обратные классы - -Для каждого класса существует "обратный ему", представленный такой же, но заглавной буквой. - -"Обратный" -- означает, что ему соответствуют все остальные символы, например: - -
    -
    `\D`
    -
    Не-цифра, то есть любой символ кроме `\d`, например буква.
    -
    `\S`
    -
    Не-пробел, то есть любой символ кроме `\s`, например буква.
    -
    `\W`
    -
    Любой символ, кроме `\w`, то есть не латинница, не подчёркивание, не цифра. В частности, русские буквы принадлежат этому классу.
    -
    `\B`
    -
    Проверка, обратная `\b`.
    -
    - -В начале этой главы мы видели, как получить из телефона +7(903)-123-45-67 все цифры. - -Первый способ -- найти все цифры через `match(/\d/g)`. - -Обратные классы помогут реализовать альтернативный -- найти все НЕцифры и удалить их из строки: - -```js -//+ run -var str = "+7(903)-123-45-67"; - -alert( str.replace(/\D/g, "") ); // 79031234567 -``` - -## Пробелы -- обычные символы - -Заметим, что в регулярных выражениях пробел -- такой же символ, как и другие. - -Обычно мы не обращаем внимание на пробелы. Для нашего взгляда строки 1-5 и 1 - 5 почти идентичны. - -Однако, если регэксп не учитывает пробелов, то он не сработает. - -Попытаемся найти цифры, разделённые дефисом: - -```js -//+ run -alert( "1 - 5".match(/\d-\d/) ); // null, нет совпадений! -``` - -Поправим это, добавив в регэксп пробелы: - -```js -//+ run -alert( "1 - 5".match(/\d - \d/) ); // работает, пробелы вокруг дефиса -``` - -Конечно же, пробелы в регэкспе нужны лишь тогда, когда мы их ищем. Лишние пробелы (как и любые лишние символы) могут навредить: - -```js -//+ run -alert( "1-5".match(/\d - \d/) ); // null, так как в строке 1-5 нет пробелов -``` - -Короче говоря, в регулярном выражении все символы имеют значение. Даже (и тем более) -- пробелы. - -## Точка -- любой символ - -Особым классом символов является точка `"."`. - -В регулярном выражении, точка "." обозначает *любой символ*, кроме перевода строки: - -```js -//+ run -alert( "Z".match(/./) ); // найдено Z -``` - -Посередине регулярного выражения: - -```js -//+ run -var re = /CS.4/; - -alert( "CSS4".match(re) ); // найдено "CSS4" -alert( "CS-4".match(re) ); // найдено "CS-4" -alert( "CS 4".match(re) ); // найдено "CS 4" (пробел тоже символ) -``` - -Обратим внимание -- точка означает именно "произвольный символ". - -То есть какой-то символ на этом месте в строке должен быть: - -```js -//+ run -alert( "CS4".match(/CS.4/) ); // нет совпадений, так как для точки нет символа -``` - -## Экранирование специальных символов - -В регулярных выражениях есть и другие символы, имеющие особый смысл. - -Они используются, чтобы расширить возможности поиска. - -Вот их полный список: [ \ ^ $ . | ? * + ( ). - -Не пытайтесь запомнить его -- когда мы разберёмся с каждым из них по отдельности, он запомнится сам собой. - -**Чтобы использовать специальный символ в качестве обычного, он должен быть *экранирован*.** - -Или, другими словами, перед символом должен быть обратный слэш `'\'`. - -Например, нам нужно найти точку '.'. В регулярном выражении она означает "любой символ, кроме новой строки", поэтому чтобы найти именно сам символ "точка" -- её нужно экранировать: \.. - -```js -//+ run -alert( "Глава 5.1".match(/\d\.\d/) ); // 5.1 -``` - -Круглые скобки также являются специальными символами, так что для поиска именно скобки нужно использовать `\(`. Пример ниже ищет строку `"g()"`: - -```js -//+ run -alert( "function g()".match(/g\(\)/) ); // "g()" -``` - -Сам символ слэш `'/'`, хотя и не является специальными символом в регулярных выражениях, но открывает-закрывает регэксп в синтаксисе /...pattern.../, поэтому его тоже нужно экранировать. - -Так выглядит поиск слэша `'/'`: - -```js -//+ run -alert( "/".match(/\//) ); // '/' -``` - -Ну и, наконец, если нам нужно найти сам обратный слэш `\`, то его нужно просто задублировать. - -Так выглядит поиск обратного слэша `"\"`: - -```js -//+ run -alert( "1\2".match(/\\/) ); // '\' -``` - - -## Итого - -Мы рассмотрели классы для поиска типов символов: - -
      -
    • `\d` -- цифры.
    • -
    • `\D` -- не-цифры.
    • -
    • `\s` -- пробельные символы, переводы строки.
    • -
    • `\S` -- всё, кроме `\s`.
    • -
    • `\w` -- латинница, цифры, подчёркивание `'_'`.
    • -
    • `'.'` -- точка обозначает любой символ, кроме перевода строки.
    • -
    - -Если хочется поискать именно сочетание `"\d"` или символ "точка", то его экранируют обратным слэшем, вот так: \. - -Заметим, что регулярное выражение может также содержать перевод строки `\n`, табуляцию `\t` и прочие спецсимволы для строк. Конфликта с классами не происходит, так как для них зарезервированы другие буквы. - - - - diff --git a/10-regular-expressions-javascript/3-regexp-character-classes/hello-java-boundaries.png b/10-regular-expressions-javascript/3-regexp-character-classes/hello-java-boundaries.png deleted file mode 100644 index 3a47f13c..00000000 Binary files a/10-regular-expressions-javascript/3-regexp-character-classes/hello-java-boundaries.png and /dev/null differ diff --git a/10-regular-expressions-javascript/3-regexp-character-classes/hello-java-boundaries@2x.png b/10-regular-expressions-javascript/3-regexp-character-classes/hello-java-boundaries@2x.png deleted file mode 100644 index e8f14d87..00000000 Binary files a/10-regular-expressions-javascript/3-regexp-character-classes/hello-java-boundaries@2x.png and /dev/null differ diff --git a/10-regular-expressions-javascript/3-regexp-character-classes/love-html5-classes.png b/10-regular-expressions-javascript/3-regexp-character-classes/love-html5-classes.png deleted file mode 100644 index 387fac9d..00000000 Binary files a/10-regular-expressions-javascript/3-regexp-character-classes/love-html5-classes.png and /dev/null differ diff --git a/10-regular-expressions-javascript/3-regexp-character-classes/love-html5-classes@2x.png b/10-regular-expressions-javascript/3-regexp-character-classes/love-html5-classes@2x.png deleted file mode 100644 index f5b3e380..00000000 Binary files a/10-regular-expressions-javascript/3-regexp-character-classes/love-html5-classes@2x.png and /dev/null differ diff --git a/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/1-find-range-1/solution.md b/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/1-find-range-1/solution.md deleted file mode 100644 index 6177c8e6..00000000 --- a/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/1-find-range-1/solution.md +++ /dev/null @@ -1,18 +0,0 @@ -Ответы: **нет, да**. - -
      -
    • В строке Java он ничего не найдёт, так как исключающие квадратные скобки в `Java[^...]` означают "один символ, кроме указанных". А после "Java" -- конец строки, символов больше нет. - -```js -//+ run -alert( "Java".match(/Java[^script]/) ); // нет совпадений -``` -
    • -
    • Да, найдёт. Поскольку регэксп регистрозависим, то под `[^script]` вполне подходит символ `"S"`. - -```js -//+ run -alert( "JavaScript".match(/Java[^script]/) ); // "JavaS" -``` -
    • -
    diff --git a/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/1-find-range-1/task.md b/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/1-find-range-1/task.md deleted file mode 100644 index 6f0b0e37..00000000 --- a/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/1-find-range-1/task.md +++ /dev/null @@ -1,5 +0,0 @@ -# Java[^script] - -Найдет ли регэксп /Java[^script]/ что-нибудь в строке Java? - -А в строке JavaScript? diff --git a/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/2-find-time-2-formats/solution.md b/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/2-find-time-2-formats/solution.md deleted file mode 100644 index 969966a2..00000000 --- a/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/2-find-time-2-formats/solution.md +++ /dev/null @@ -1,9 +0,0 @@ -Ответ: \d\d[-:]\d\d. - -```js -//+ run -var re = /\d\d[-:]\d\d/g; -alert( "Завтрак в 09:00. Обед - в 21-30".match(re) ); -``` - -Обратим внимание, что дефис '-' не экранирован, поскольку в начале скобок он не может иметь специального смысла. diff --git a/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/2-find-time-2-formats/task.md b/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/2-find-time-2-formats/task.md deleted file mode 100644 index dd03af58..00000000 --- a/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/2-find-time-2-formats/task.md +++ /dev/null @@ -1,11 +0,0 @@ -# Найдите время в одном из форматов - -Время может быть в формате `часы:минуты` или `часы-минуты`. И часы и минуты состоят из двух цифр, например `09:00`, `21-30`. - -Напишите регулярное выражение для поиска времени: - -```js -var re = /ваше выражение/; -alert( "Завтрак в 09:00. Обед - в 21-30".match(re) ); // 09:00, 21-30 -``` - diff --git a/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/article.md b/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/article.md deleted file mode 100644 index c8f2041f..00000000 --- a/10-regular-expressions-javascript/4-regexp-character-sets-and-ranges/article.md +++ /dev/null @@ -1,170 +0,0 @@ -# Наборы и диапазоны [...] - -Если в регулярном выражении несколько символов или символьных классов заключены в квадратные скобки `[…]`, то это означает "искать любой символ из указанных в `[…]`". - -[cut] - -## Набор - -Например, [еао] означает любой символ из этих трёх: `'а'`, `'е'`, или `'о'`. - -Такое обозначение называют *набором*. Наборы используются в регулярном выражении наравне с обычными символами: - -```js -//+ run -// найти [г или т], а затем "оп" -alert( "Гоп-стоп".match(/[гт]оп/gi) ); // "Гоп", "топ" -``` - -Обратим внимание: несмотря на то, что в наборе указано несколько символов, в совпадении должен присутствовать *ровно один* из них. - -Поэтому в примере ниже нет результатов: - -```js -//+ run -// найти "В", затем [у или а], затем "ля" -alert( "Вуаля".match(/В[уа]ля/) ); // совпадений нет -``` - -Поиск подразумевает: -
      -
    • В,
    • -
    • затем *одну* из букв набора [уа],
    • -
    • а затем ля
    • -
    - -Таким образом, совпадение было бы для строки Вуля или Валя. - -## Диапазоны - -Квадратные скобки могут также содержать *диапазоны символов*. - -Например, [a-z] -- произвольный символ от `a` до `z`, [0-5] -- цифра от `0` до `5`. - -В примере ниже мы будем искать `"x"`, после которого идёт два раза любая цифра или буква от A до F: - -```js -//+ run -// найдёт "xAF" -alert( "Exception 0xAF".match(/x[0-9A-F][0-9A-F]/g) ); -``` - -Обратим внимание, в слове Exception есть сочетание xce, но оно не подошло, потому что буквы в нём маленькие, а в диапазоне [0-9A-F] -- большие. - -Если хочется искать и его тоже, можно добавить в скобки диапазон `a-f`: [0-9A-Fa-f]. Или же просто указать у всего регулярного выражения флаг `i`. - -**Символьные классы -- всего лишь более короткие записи для диапазонов, в частности:** - -
      -
    • **\d** -- то же самое, что [0-9],
    • -
    • **\w** -- то же самое, что [a-zA-Z0-9_],
    • -
    • **\s** -- то же самое, что [\t\n\v\f\r ] плюс несколько юникодных пробельных символов.
    • -
    - -В квадратных скобках можно использовать и диапазоны и символьные классы -- вместе. - -Например, нам нужно найти все слова в тексте. Если они на английском -- это достаточно просто: - -```js -//+ run -var str = "The sun is rising!"; - -alert( str.match(/\w+/g) ); // The, sun, is, rising*!* -``` - -А если есть слова и на русском? - -```js -//+ run -var str = "Солнце встаёт!"; - -alert( str.match(/\w+/g) ); // null*!* -``` - -Ничего не найдено! Это можно понять, ведь \w -- это именно английская букво-цифра, как можно видеть из аналога [a-zA-Z0-9_]. - -Чтобы находило слово на русском -- нужно использовать диапазон, например /[а-я]/. - -А чтобы на обоих языках -- и то и другое вместе: - -```js -//+ run -var str = "Солнце (the sun) встаёт!"; - -alert( str.match(/[\wа-я]+/gi) ); // Солнце, the, sun, вста, т*!* -``` - -...Присмотритесь внимательно к предыдущему примеру! Вы видите странность? Оно не находит букву ё, более того -- считает её разрывом в слове. Причина -- в кодировке юникод, она подробно раскрыта в главе [](/string). - -Буква `ё` лежит в стороне от основной кириллицы и её следует добавить в диапазон дополнительно, вот так: - -```js -//+ run -var str = "Солнце (the sun) встаёт!"; - -alert( str.match(/[\wа-яё]+/gi) ); // Солнце, the, sun, встаёт*!* -``` - -Теперь всё в порядке. - -## Диапазоны "кроме" - -**Кроме обычных, существуют также *исключающие* диапазоны: [^…].** - -Квадратные скобки, начинающиеся со знака каретки: [^…] находят любой символ, *кроме указанных*. - -Например: - -
      -
    • [^аеуо] -- любой символ, кроме `'a'`, `'e'`, `'y'`, `'o'`.
    • -
    • [^0-9] -- любой символ, кроме цифры, то же что `\D`.
    • -
    • [^\s] -- любой не-пробельный символ, то же что `\S`.
    • -
    - -Пример ниже ищет любые символы, кроме букв, цифр и пробелов: - -```js -//+ run -alert( "alice15@gmail.com".match(/[^\d\sA-Z]/gi) ); // "@", "." -``` - -## Не нужно экранирование - -Обычно, если мы хотим искать именно точку, а не любой символ, или именно символ `\`, то мы используем экранирование: указываем `\.` или `\\`. - -В квадратных скобках большинство специальных символов можно использовать без экранирования, если конечно они не имеют какой-то особый смысл именно внутри квадратных скобок. - -То есть, "как есть", без экранирования можно использовать символы: -
      -
    • Точка '.'.
    • -
    • Плюс '+'.
    • -
    • Круглые скобки '( )'.
    • -
    • Дефис '-', если он находится в начале или конце квадратных скобок, то есть не выделяет диапазон.
    • -
    • Символ каретки '^', если не находится в начале квадратных скобок.
    • -
    • А также открывающая квадратная скобка '['.
    • -
    - -То есть, точка `"."` в квадратных скобках означает не "любой символ", а обычную точку. - -Регэксп [.,] ищет один из символов "точка" или "запятая". - -В примере ниже регэксп [-().^+] ищет один из символов `-().^`. Они не экранированы: - -```js -//+ run -// Без экранирования -var re = /[-().^+]/g; - -alert( "1 + 2 - 3".match(re) ); // найдёт +, - -``` - -...Впрочем, даже если вы решите "на всякий случай" заэкранировать эти символы, поставив перед ними обратный слэш `\` -- вреда не будет: - -```js -//+ run -// Всё заэкранировали -var re = /[\-\(\)\.\^\+]/g; - -alert( "1 + 2 - 3".match(re) ); // тоже работает: +, - -``` - diff --git a/10-regular-expressions-javascript/5-regexp-quantifiers/1-find-text-manydots/solution.md b/10-regular-expressions-javascript/5-regexp-quantifiers/1-find-text-manydots/solution.md deleted file mode 100644 index 5108d628..00000000 --- a/10-regular-expressions-javascript/5-regexp-quantifiers/1-find-text-manydots/solution.md +++ /dev/null @@ -1,11 +0,0 @@ - -Решение: - -```js -//+ run -var reg = /\.{3,}/g; -alert( "Привет!... Как дела?.....".match(reg) ); // ..., ..... -``` - -Заметим, что символ `.` является специальным, значит его надо экранировать, то есть вставлять как `\.`. - diff --git a/10-regular-expressions-javascript/5-regexp-quantifiers/1-find-text-manydots/task.md b/10-regular-expressions-javascript/5-regexp-quantifiers/1-find-text-manydots/task.md deleted file mode 100644 index 4e94ba20..00000000 --- a/10-regular-expressions-javascript/5-regexp-quantifiers/1-find-text-manydots/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Как найти многоточие... ? - -[importance 5] - -Напишите регулярное выражения для поиска многоточий: трёх или более точек подряд. - -Проверьте его: - -```js -var reg = /ваше выражение/g; -alert( "Привет!... Как дела?.....".match(reg) ); // ..., ..... -``` - diff --git a/10-regular-expressions-javascript/5-regexp-quantifiers/2-find-html-colors-6hex/solution.md b/10-regular-expressions-javascript/5-regexp-quantifiers/2-find-html-colors-6hex/solution.md deleted file mode 100644 index 09576d99..00000000 --- a/10-regular-expressions-javascript/5-regexp-quantifiers/2-find-html-colors-6hex/solution.md +++ /dev/null @@ -1,34 +0,0 @@ -Итак, нужно написать выражение для описания цвета, который начинается с "#", за которым следуют 6 шестнадцатеричных символов. - -Шестнадцатеричный символ можно описать с помощью [0-9a-fA-F]. Мы можем сократить выражение, используя не чувствительный к регистру шаблон [0-9a-f]. - -Для его шестикратного повторения мы будем использовать квантификатор {6}. - -В итоге, получаем выражение вида /#[a-f0-9]{6}/gi. - -```js -//+ run -var re = /#[a-f0-9]{6}/gi; - -var str = "color:#121212; background-color:#AA00ef bad-colors:f#fddee #fd2"; - -alert( str.match(re) ); // #121212,#AA00ef -``` - -Проблема этого выражения в том, что оно находит цвет и в более длинных последовательностях: - -```js -//+ run -alert( "#12345678".match( /#[a-f0-9]{6}/gi ) ) // #12345678 -``` - -Чтобы такого не было, можно добавить в конец `\b`: - -```js -//+ run -// цвет -alert( "#123456".match( /#[a-f0-9]{6}\b/gi ) ); // #123456 - -// не цвет -alert( "#12345678".match( /#[a-f0-9]{6}\b/gi ) ); // null -``` diff --git a/10-regular-expressions-javascript/5-regexp-quantifiers/2-find-html-colors-6hex/task.md b/10-regular-expressions-javascript/5-regexp-quantifiers/2-find-html-colors-6hex/task.md deleted file mode 100644 index ea2dbf18..00000000 --- a/10-regular-expressions-javascript/5-regexp-quantifiers/2-find-html-colors-6hex/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Регулярное выражение для цвета - -Напишите регулярное выражение для поиска HTML-цвета, заданного как `#ABCDEF`, то есть `#` и содержит затем 6 шестнадцатеричных символов. - -Пример использования: - -``` -var re = /*...ваше регулярное выражение...*/ - -var str = "color:#121212; background-color:#AA00ef bad-colors:f#fddee #fd2" - -alert( str.match(re) ) // #121212,#AA00ef -``` - diff --git a/10-regular-expressions-javascript/5-regexp-quantifiers/3-find-decimal-positive-numbers/solution.md b/10-regular-expressions-javascript/5-regexp-quantifiers/3-find-decimal-positive-numbers/solution.md deleted file mode 100644 index 41f71b50..00000000 --- a/10-regular-expressions-javascript/5-regexp-quantifiers/3-find-decimal-positive-numbers/solution.md +++ /dev/null @@ -1,18 +0,0 @@ - - -Целое число -- это \d+. - -Десятичная точка с дробной частью -- \.\d+. - -Она не обязательна, так что обернём её в скобки с квантификатором '?'. - -Итого, получилось регулярное выражение \d+(\.\d+)?: - -```js -//+ run -var re = /\d+(\.\d+)?/g - -var str = "1.5 0 12. 123.4."; - -alert( str.match(re) ); // 1.5, 0, 12, 123.4 -``` diff --git a/10-regular-expressions-javascript/5-regexp-quantifiers/3-find-decimal-positive-numbers/task.md b/10-regular-expressions-javascript/5-regexp-quantifiers/3-find-decimal-positive-numbers/task.md deleted file mode 100644 index 17e4d836..00000000 --- a/10-regular-expressions-javascript/5-regexp-quantifiers/3-find-decimal-positive-numbers/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Найдите положительные числа - -Создайте регэксп, который ищет все положительные числа, в том числе и с десятичной точкой. - -Пример использования: - -```js -var re = /* ваш регэксп */ - -var str = "1.5 0 12. 123.4."; - -alert( str.match(re) ); // 1.5, 0, 12, 123.4 -``` \ No newline at end of file diff --git a/10-regular-expressions-javascript/5-regexp-quantifiers/4-find-decimal-numbers/solution.md b/10-regular-expressions-javascript/5-regexp-quantifiers/4-find-decimal-numbers/solution.md deleted file mode 100644 index 2abd91c0..00000000 --- a/10-regular-expressions-javascript/5-regexp-quantifiers/4-find-decimal-numbers/solution.md +++ /dev/null @@ -1,13 +0,0 @@ -Целое число с необязательной дробной частью -- это \d+(\.\d+)?. - -К этому нужно добавить необязательный `-` в начале: - - -```js -//+ run -var re = /-?\d+(\.\d+)?/g - -var str = "-1.5 0 2 -123.4."; - -alert( str.match(re) ); // -1.5, 0, 2, -123.4 -``` diff --git a/10-regular-expressions-javascript/5-regexp-quantifiers/4-find-decimal-numbers/task.md b/10-regular-expressions-javascript/5-regexp-quantifiers/4-find-decimal-numbers/task.md deleted file mode 100644 index 5c5d4d09..00000000 --- a/10-regular-expressions-javascript/5-regexp-quantifiers/4-find-decimal-numbers/task.md +++ /dev/null @@ -1,13 +0,0 @@ -# Найдите десятичные числа - -Создайте регэксп, который ищет все числа, в том числе и с десятичной точкой, в том числе и отрицательные. - -Пример использования: - -```js -var re = /* ваш регэксп */ - -var str = "-1.5 0 2 -123.4."; - -alert( str.match(re) ); // -1.5, 0, 2, -123.4 -``` \ No newline at end of file diff --git a/10-regular-expressions-javascript/5-regexp-quantifiers/article.md b/10-regular-expressions-javascript/5-regexp-quantifiers/article.md deleted file mode 100644 index 17605289..00000000 --- a/10-regular-expressions-javascript/5-regexp-quantifiers/article.md +++ /dev/null @@ -1,168 +0,0 @@ -# Квантификаторы +, *, ? и {n} - -Рассмотрим ту же задачу, что и ранее -- взять телефон вида `+7(903)-123-45-67` и найти все числа в нём. Но теперь нас интересуют не цифры по отдельности, а именно числа, то есть результат вида `7, 903, 123, 45, 67`. - -Для поиска цифр по отдельности нам было достаточно класса `\d`. Но здесь нужно искать *числа* -- последовательности из 1 или более цифр. - -## Количество {n} - -Количество повторений символа можно указать с помощью числа в фигурных скобках: `{n}`. - -Такое указание называют *квантификатором* (от англ. quantifier). - -У него есть несколько подформ записи: - -
    -
    Точное количество: `{5}`
    -
    Регэксп \d{5} обозначает ровно 5 цифр, в точности как \d\d\d\d\d. - -Следующий пример находит пятизначное число. - -```js -//+ run -alert( "Мне 12345 лет".match(/\d{5}/) ); // "12345" -``` - -
    -
    Количество от-до: `{3,5}`
    -
    Для того, чтобы найти, например, числа размером от трёх до пяти знаков, нужно указать границы в фигурных скобках: \d{3,5} - -```js -//+ run -alert( "Мне не 12, а 1234 года".match(/\d{3,5}/) ); // "1234" -``` - -Последнее значение можно и не указывать. Тогда выражение \d{3,} найдет числа, длиной от трех цифр: - -```js -//+ run -alert( "Мне не 12, а 345678 лет".match(/\d{3,5}/) ); // "345678" -``` -
    -
    - -В случае с телефоном нам нужны числа -- одна или более цифр подряд. Этой задаче соответствует регулярное выражение \d{1,}: - -```js -//+ run -var str = "+7(903)-123-45-67"; - -alert( str.match(/\d{1,}/g) ); // 7,903,123,45,67 -``` - - -## Короткие обозначения - -Для самые часто востребованных квантификаторов есть специальные короткие обозначения. - -
    -
    `+`
    -
    Означает "один или более", то же что `{1,}`. - -Например, \d+ находит числа -- последовательности из 1 или более цифр: - -```js -//+ run -var str = "+7(903)-123-45-67"; - -alert( str.match(/\d+/g) ); // 7,903,123,45,67 -``` - -
    -
    `?`
    -
    Означает "ноль или один", то же что и `{0,1}`. По сути, делает символ необязательным. - -Например, регэксп ou?r найдёт o, после которого, возможно, следует u, а затем r. - -Этот регэксп найдёт or в слове color и our в colour: - -```js -//+ run -var str = "Можно писать color или colour (британский вариант)"; - -alert( str.match(/colou?r/g) ); // color, colour -``` - -
    -
    `*`
    -
    Означает "ноль или более", то же что `{0,}`. То есть, символ может повторяться много раз или вообще отсутствовать. - -Пример ниже находит цифру, после которой идёт один или более нулей: - -```js -//+ run -alert( "100 10 1".match(/\d0*/g) ); // 100, 10, 1 -``` - -Сравните это с `'+'` (один или более): - -```js -//+ run -alert( "100 10 1".match(/\d0+/g) ); // 100, 10 -``` - -
    -
    - -## Ещё примеры - -Эти квантификаторы принадлежат к числу самых важных "строительных блоков" для сложных регулярных выражений, поэтому мы рассмотрим ещё примеры. - -
    -
    Регэксп "десятичная дробь" (число с точкой внутри): \d+\.\d+
    -
    - -В действии: -```js -//+ run -alert( "0 1 12.345 7890".match(/\d+\.\d+/g) ); // 12.345 -``` - -
    -
    Регэксп "открывающий HTML-тег без атрибутов", такой как `` или `

    `: /<[a-z]+>/i

    -
    Пример: - -```js -//+ run -alert( " ... ".match(/<[a-z]+>/gi) ); // -``` - -Это регулярное выражение ищет символ '<', за которым идут одна или более букв английского алфавита, и затем '>'. -
    -
    Регэксп "открывающий HTML-тег без атрибутов" (лучше): /<[a-z][a-z0-9]*>/i
    -
    -Здесь регулярное выражение расширено: в соответствие со стандартом, HTML-тег может иметь символ на любой позиции, кроме первой, например `

    `. - -```js -//+ run -alert( "

    Привет!

    ".match(/<[a-z][a-z0-9]*>/gi) ); //

    -``` - -

    -
    Регэксп "открывающий или закрывающий HTML-тег без атрибутов": /<\/?[a-z][a-z0-9]*>/i
    -
    В предыдущий паттерн добавили необязательный слэш /? перед тегом. Его понадобилось заэкранировать, чтобы JavaScript не принял его за конец шаблона. - -```js -//+ run -alert( "

    Привет!

    ".match(/<\/?[a-z][a-z0-9]*>/gi) ); //

    ,

    -``` - -
    -
    - - -[smart header="Точнее -- значит сложнее"] -В этих примерах мы видим общее правило, которое повторяется из раза в раз: чем точнее регулярное выражение, тем оно длиннее и сложнее. - -Например, для HTML-тегов, скорее всего, подошло бы и более короткое регулярное выражение <\w+>. - -Так как класс `\w` означает "любая цифра или английская буква или `'_'`, то под такой регэксп подойдут и не теги, например <_>. Однако он гораздо проще, чем более точный регэксп <[a-z][a-z0-9]*>. - -Подойдёт ли нам <\w+> или нужно использовать именно <[a-z][a-z0-9]*>? - -В реальной жизни допустимы оба варианта. Ответ на подобные вопросы зависит от того, насколько реально важна точность и насколько сложно потом будет отфильтровать лишние совпадения (если появятся). -[/smart] - - - - diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/1-lazy-greedy/solution.md b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/1-lazy-greedy/solution.md deleted file mode 100644 index 083b3044..00000000 --- a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/1-lazy-greedy/solution.md +++ /dev/null @@ -1,6 +0,0 @@ - -Результат: `123 456`. - -Ленивый `\d+?` будет брать цифры до пробела, то есть `123`. После каждой цифры он будет останавливаться, проверять -- не пробел ли дальше? Если нет -- брать ещё цифру, в итоге возьмёт `123`. - -З в дело вступит `\d+`, который по-максимуму возьмёт дальнейшие цифры, то есть `456`. \ No newline at end of file diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/1-lazy-greedy/task.md b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/1-lazy-greedy/task.md deleted file mode 100644 index 7076396f..00000000 --- a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/1-lazy-greedy/task.md +++ /dev/null @@ -1,8 +0,0 @@ -# Совпадение для /d+? d+/ - -Что будет при таком поиске, когда сначало стоит ленивый, а потом жадный квантификаторы? - -```js -"123 456".match(/\d+? \d+/g) ); // какой результат? -``` - diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/2-difference-find-quote/solution.md b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/2-difference-find-quote/solution.md deleted file mode 100644 index b47d89b3..00000000 --- a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/2-difference-find-quote/solution.md +++ /dev/null @@ -1,16 +0,0 @@ -Они очень похожи и, да, *почти* одинаковы. Оба ищут от одной кавычки до другой. - -Различие здесь в символе точка '.'. Как мы помним, точка '.' обозначает *любой символ, кроме перевода строки*. - -А [^"] -- это *любой символ, кроме кавычки '"'. - -Получатся, что первый регэксп "[^"]*" найдёт закавыченные строки с `\n` внутри, а второй регэксп ".*?" -- нет. - -Вот пример: -```js -//+ run -alert( '"многострочный \n текст"'.match(/"[^"]*"/) ); // найдёт - -alert( '"многострочный \n текст"'.match(/".*?"/) ); // null (нет совпадений) -``` - diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/2-difference-find-quote/task.md b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/2-difference-find-quote/task.md deleted file mode 100644 index 5e9dcf17..00000000 --- a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/2-difference-find-quote/task.md +++ /dev/null @@ -1,5 +0,0 @@ -# Различие между "[^"]*" и ".*?" - -Регулярные выражения "[^"]*" и ".*?" -- при выполнении одинаковы? - -Иначе говоря, существует ли такая строка, на которой они дадут разные результаты? Если да -- дайте такую строку. diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/3-find-html-comments/solution.md b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/3-find-html-comments/solution.md deleted file mode 100644 index 17bf43eb..00000000 --- a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/3-find-html-comments/solution.md +++ /dev/null @@ -1,18 +0,0 @@ -Нужно найти начало комментария <!--, затем всё до конца -->. - -С первого взгляда кажется, что это сделает регулярное выражение <!--.*?--> -- квантификатор сделан ленивым, чтобы остановился, достигнув -->. - -Однако, точка в JavaScript -- любой символ, *кроме* конца строки. Поэтому такой регэксп не найдёт многострочный комментарий. - -Всё получится, если вместо точки использовать полностю "всеядный" [\s\S]. - -Итого: - -```js -//+ run -var re = //g; - -var str = '.. .. .. '; - -alert( str.match(re) ); // '', '' -``` \ No newline at end of file diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/3-find-html-comments/task.md b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/3-find-html-comments/task.md deleted file mode 100644 index af0b1b2a..00000000 --- a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/3-find-html-comments/task.md +++ /dev/null @@ -1,11 +0,0 @@ -# Найти HTML-комментарии - -Найдите все HTML-комментарии в тексте: - -```js -var re = ..ваш регэксп.. - -var str = '.. .. .. '; - -alert( str.match(re) ); // '', '' -``` diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/4-find-html-tags-greedy-lazy/solution.md b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/4-find-html-tags-greedy-lazy/solution.md deleted file mode 100644 index 6a87a597..00000000 --- a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/4-find-html-tags-greedy-lazy/solution.md +++ /dev/null @@ -1,38 +0,0 @@ -Начнём поиск с <, затем один или более произвольный символ, но до закрывающего "уголка": .+?>. - -Проверим, как работает этот регэксп: - -```js -//+ run -var re = /<.+?>/g; - -var str = '<>
    '; - -alert( str.match(re) ); // <> , , -``` - -Результат неверен! В качестве первого тега регэксп нашёл подстроку <> <a href="/">, но это явно не тег. - -Всё потому, что .+? -- это "любой символ (кроме `\n`), повторяющийся один и более раз до того, как оставшаяся часть шаблона совпадёт (ленивость)". - -Поэтому он находит первый `<`, затем есть "всё подряд" до следующего `>`. - -Первое совпадение получается как раз таким: - -``` -<.............> -<> -``` - -Правильным решением будет использовать <[^>]+>: - -```js -//+ run -var re = /<[^>]+>/g - -var str = '<> '; - -alert( str.match(re) ); // , , -``` - -Это же решение автоматически позволяет находится внутри тегу символу `\n`, который в класс точка `.` не входит. \ No newline at end of file diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/4-find-html-tags-greedy-lazy/task.md b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/4-find-html-tags-greedy-lazy/task.md deleted file mode 100644 index 43935130..00000000 --- a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/4-find-html-tags-greedy-lazy/task.md +++ /dev/null @@ -1,18 +0,0 @@ -# Найти HTML-теги - -Создайте регулярное выражение для поиска всех (открывающихся и закрывающихся) HTML-тегов вместе с атрибутами. - -Пример использования: -```js -//+ run -var re = /* ваш регэксп */ - -var str = '<> '; - -alert( str.match(re) ); // '', '', '' -``` - -В этой задаче можно считать, что тег начинается с <, заканчивается > и может содержать внутри любые символы, кроме < и >. - -Но хотя бы один символ внутри тега должен быть: <> -- не тег. - diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/article.md b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/article.md deleted file mode 100644 index 09758bab..00000000 --- a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/article.md +++ /dev/null @@ -1,335 +0,0 @@ -# Жадные и ленивые квантификаторы - -Квантификаторы -- с виду очень простая, но на самом деле очень хитрая штука. - -Необходимо очень хорошо понимать, как именно происходит поиск, если конечно мы хотим искать что-либо сложнее чем /\d+/. - -[cut] - -Для примера рассмотрим задачу, которая часто возникает в типографике -- заменить в тексте кавычки вида `"..."` (их называют "английские кавычки") на "кавычки-ёлочки": `«...»`. - -Для этого нужно сначала найти все слова в таких кавычках. - -Соотверствующее регулярное выражение может выглядеть так: /".+"/g, то есть мы ищем кавычку, после которой один или более произвольный символ, и в конце опять кавычка. - -Однако, если попробовать применить его на практике, даже на таком простом случае... - -```js -//+ run -var reg = /".+"/g; - -var str = 'a "witch" and her "broom" is one'; - -alert( str.match(reg) ); // "witch" and her "broom" -``` - -...Мы увидим, что оно работает совсем не так, как задумано! - -Вместо того, чтобы найти два совпадения "witch" и "broom", оно находит одно: "witch" and her "broom". - -Это как раз тот случай, когда *жадность* -- причина всех зол. - -## Жадный поиск - -Чтобы найти совпадение, движок регулярных выражений обычно использует следующий алгоритм: - -
      -
    • Для каждой позиции в поисковой строке -
        -
      • Проверить совпадение на данной позиции -
        • Посимвольно, с учётом классов и квантификаторов сопоставив с ней регулярное выражение.
        -
      • -
      -
    • -
    - -Это общие слова, гораздо понятнее будет, если мы проследим, что именно он делает для регэкспа ".+". - -
      -
    1. Первый символ шаблона -- это кавычка ". - -Движок регулярных выражений пытается сопоставить её на 0й позиции в строке, но символ `a`, поэтому на 0й позиции соответствия явно нет. - -Далее он переходит 1ю, 2ю позицию в исходной строке и, наконец, обнаруживает кавычку на 3й позиции: - -
    2. -
    3. Кавычка найдена, далее движок проверяет, есть ли соответствие для остальной части паттерна. - -В данном случае следующий символ шаблона: . (точка). Она обозначает "любой символ", так что следующая буква строки 'w' вполне подходит: - -
    4. -
    5. Далее "любой символ" повторяется, так как стоит квантификатор .+. Движок регулярных выражений берёт один символ за другим, до тех пор, пока у него это получается. - -В данном случае это означает "до конца строки": - -
    6. -
    7. Итак, текст закончился, движок регулярных выражений больше не может найти "любой символ", он закончил повторения для .+ и переходит к следующему символу шаблона. - -Следующий символ шаблона -- это кавычка. Её тоже необходимо найти, чтобы соответствие было полным. А тут -- беда, ведь поисковый текст завершился! - -Движок регулярных выражений понимает, что, наверное, взял многовато .+ и начинает отступать обратно. - -Иными словами, он сокращает текущее совпадение на один символ: - - - -Это называется "фаза возврата" или "фаза бэктрекинга" (backtracking -- англ.). - -Теперь .+ соответствует почти вся оставшаяся строка, за исключением одного символа, и движок регулярных выражений ещё раз пытается подобрать соответствие для остатка шаблона, начиная с оставшейся части строки. - -Если бы последним символом строки была кавычка '"', то на этом бы всё и закончилось. Но последний символ 'e', так что совпадения нет.
    8. -
    9. ...Поэтому движок уменьшает число повторений .+ ещё на один символ: - - - -Кавычка '"' не совпадает с 'n'. Опять неудача.
    10. -
    11. Движок продолжает отступать, он уменьшает количество повторений точки '.' до тех пор, пока остаток паттерна, то есть в данном случае кавычка '"', не совпадёт: - - -
    12. -
    13. Совпадение получено. Дальнейший поиск по оставшейся части строки is one новых совпадений не даст.
    14. -
    - -Возможно, это не совсем то, что мы ожидали. - -**В жадном режиме (по умолчанию) регэксп повторяет квантификатор настолько много раз, насколько это возможно, чтобы найти соответствие.** - -То есть, любой символ .+ повторился максимальное количество раз, что и привело к такой длинной строке. - -А мы, наверное, хотели, чтобы каждая строка в кавычках была независимым совпадением? Для этого можно переключить квантификатор `+` в "ленивый" режим, о котором будет речь далее. - -## Ленивый режим - -Ленивый режим работы квантификаторов -- противоположность жадному, он означает "повторять минимальное количество раз". - -Его можно включить, если поставить знак вопроса '?' после квантификатора, так что он станет таким: *? или +? или даже ?? для '?'. - -Чтобы не возникло путаницы -- важно понимать: обычно `?` сам является квантификатором (ноль или один). Но если он стоит *после другого квантификатора (или даже после себя)*, то обретает другой смысл -- в этом случае он меняет режим его работы на ленивый. - -Регэксп /".+?"/g работает, как задумано -- находит отдельно witch и broom: - -```js -//+ run -var reg = /".+?"/g; - -var str = 'a "witch" and her "broom" is one'; - -alert( str.match(reg) ); // witch, broom -``` - -Чтобы в точности понять, как поменялась работа квантификатора, разберём поиск по шагам. - -
      -
    1. Первый шаг -- тот же, кавычка '"' найдена на 3й позиции: - -
    2. - -
    3. Второй шаг -- тот же, находим произвольный символ '.': - -
    4. - -
    5. А вот дальше -- так как стоит ленивый режим работы `+`, то движок не повторет точку (произвольный символ) ещё раз, а останавливается на достигнутом и пытается проверить, есть ли соответствие остальной части шаблона, то есть '"': - - -Если бы остальная часть шаблона на данной позиции совпала, то совпадение было бы найдено. Но в данном случе -- нет, символ `'i'` не равен '"'. -
    6. -
    7. Движок регулярных выражений увиличивает количество повторений точки на одно и пытается найти соответствие остатку шаблона ещё раз: - - -Опять неудача. Тогда поисковой движок увеличивает количество повторений ещё и ещё... -
    8. -
    9. Только на 5м шаге поисковой движок наконец находит соответствие для остатка паттерна: - - -
    10. -
    11. Так как поиск происходит с флагом `g`, то он продолжается с конца текущего совпадения, давая ещё один результат: - - -
    12. -
    - -В примере выше продемонстрирована работа ленивого режима для +?. Квантификаторы +? и ?? ведут себя аналогично -- "ленивый" движок увеличивает количество повторений только в том случае, если для остальной части шаблона на данной позиции нет соответствия. - -**Ленивость распространяется только на тот квантификатор, после которого стоит `?`.** - -Прочие квантификаторы остаются жадными. - -Например: - -```js -//+ run -alert( "123 456".match(/\d+ \d+?/g) ); // 123 4 -``` - -
      -
    1. Подшаблон \d+ пытается найти столько цифр, сколько возможно (работает жадно), так что он находит 123 и останавливается, поскольку символ пробела ' ' не подходит под \d.
    2. -
    3. Далее в шаблоне пробел, он совпадает.
    4. -
    5. Далее в шаблоне идёт \d+?. - -Квантификатор указан в ленивом режиме, поэтому он находит одну цифру 4 и пытается проверить, есть ли совпадение с остатком шаблона. - -Но после \d+? в шаблоне ничего нет. - -**Ленивый режим без необходимости лишний раз квантификатор не повторит.** - -Так как шаблон завершился, то искать дальше, в общем-то нечего. Получено совпадение 123 4.
    6. -
    7. Следующий поиск продолжится с `5`, но ничего не найдёт.
    8. -
    - -[smart header="Конечные автоматы и не только"] -Современные движки регулярных выражений могут иметь более хитрую реализацию внутренних алгоритмов, чтобы искать быстрее. - -Однако, чтобы понять, как работает регулярное выражение, и строить регулярные выражения самому, знание этих хитрых алгоритмов ни к чему. Они служат лишь внутренней оптимизации способа поиска, описанного выше. - -Кроме того, сложные регулярные выражения плохо поддаются всяким оптимизациям, так что поиск вполне может работать и в точности как здесь описано. -[/smart] - -## Альтернативный подход - -В данном конкретном случае, возможно искать строки в кавычках, оставаясь в жадном режиме, с использованием регулярного выражения "[^"]+": - -```js -//+ run -var reg = /"[^"]+"/g; - -var str = 'a "witch" and her "broom" is one'; - -alert( str.match(reg) ); // witch, broom -``` - -Регэксп "[^"]+" даст правильные результаты, поскольку ищет кавычку '"', за которой идут столько не-кавычек (исключающие квадратные скобки), сколько возможно. - -Так что вторая кавычка автоматически прекращает повторения [^"]+ и позволяет найти остаток шаблона ". - -**Эта логика ни в коей мере не заменяет ленивые квантификаторы!** - - -Она просто другая. И то и другое бывает полезно. - -Давайте посмотрим пример, когда нужен именно такой вариант, а ленивые квантификаторы не подойдут. - -Например, мы хотим найти в тексте ссылки вида `
    `, с любым содержанием `href`. - -Какое регулярное выражение для этого подойдёт? - -Первый вариант может выглядеть так: /<a href=".*" class="doc">/g. - -Проверим его: -```js -//+ run -var str = '......'; -var reg = //g; - -// Сработало! -alert( str.match(reg) ); // -``` - -А если в тексте несколько ссылок? - -```js -//+ run -var str = '...... ...'; -var reg = //g; - -// Упс! Сразу две ссылки! -alert( str.match(reg) ); // ... -``` - -На этот раз результат неверен. - -Жадный .* взял слишком много символов. - -Соответствие получилось таким: -``` - -... -``` - -Модифицируем шаблон -- добавим ленивость квантификатору .*?: - -```js -//+ run -var str = '...... ...'; -var reg = //g; - -// Сработало! -alert( str.match(reg) ); // , -``` - -Теперь всё верно, два результата: - -``` - -... -``` - -Почему теперь всё в порядке -- для внимательного читателя, после объяснений, данных выше в этой главе, должно быть полностью очевидно. - -Поэтому не будем останавливаться здесь на деталях, а попробуем ещё пример: - -```js -//+ run -var str = '......

    ...'; -var reg = //g; - -// Неправильное совпадение! -alert( str.match(reg) ); // ...

    -``` - -Совпадение -- не ссылка, а более длинный текст. - -Получилось следующее: -

      -
    1. Найдено совпадение <a href=".
    2. -
    3. Лениво ищем .*?, после каждого символа проверяя, есть ли совпадение остальной части шаблона. - -Подшаблон .*? будет брать символы до тех пор, пока не найдёт class="doc">. - -В данном случае этот поиск закончится уже за пределами ссылки, в теге `

      `, вообще не имеющем отношения к ``. -

    4. -
    5. Получившееся совпадение: - -``` - -...

      -``` -

    6. -
    - -Итак, ленивость нам не помогла. - -Необходимо как-то прекратить поиск .*, чтобы он не вышел за пределы кавычек. - -Для этого мы используем более точное указание, какие символы нам подходят, а какие нет. - -Правильный вариант: [^"]*. Этот шаблон будет брать все символы до ближайшей кавычки, как раз то, что требуется. - -Рабочий пример: - -```js -//+ run -var str1 = '......

    ...'; -var str2 = '...... ...'; -var reg = //g; - -// Работает! -alert( str1.match(reg) ); // null, совпадений нет, и это верно -alert( str2.match(reg) ); // , -``` - -## Итого - -Квантификаторы имеют два режима работы: -

    -
    Жадный
    -
    Режим по умолчанию -- движок регулярных выражений повторяет его по-максимуму. Когда повторять уже нельзя, например нет больше цифр для `\d+`, он продолжает поиск с оставшейся части текста. Если совпадение найти не удалось -- отступает обратно, уменьшая количество повторений.
    -
    Ленивый
    -
    При указании после квантификатора символа `?` он работает в ленивом режиме. То есть, он перед каждым повторением проверяет совпадение оставшейся части шаблона на текущей позиции.
    -
    - -Как мы видели в примере выше, ленивый режим -- не панацея от "слишком жадного" забора символов. Альтернатива -- более аккуратно настроенный "жадный", с исключением символов. Как мы увидим далее, можно исключать не только символы, но и целые подшаблоны. - - - - - diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy1.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy1.png deleted file mode 100644 index a4fc2c4f..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy1.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy1@2x.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy1@2x.png deleted file mode 100644 index e1b2ca0a..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy1@2x.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy2.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy2.png deleted file mode 100644 index b7b84c1b..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy2.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy2@2x.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy2@2x.png deleted file mode 100644 index ce87d89e..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy2@2x.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy3.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy3.png deleted file mode 100644 index 0e5b07fd..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy3.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy3@2x.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy3@2x.png deleted file mode 100644 index 975fc49c..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy3@2x.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy4.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy4.png deleted file mode 100644 index 0ca3df76..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy4.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy4@2x.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy4@2x.png deleted file mode 100644 index fe514d4d..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy4@2x.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy5.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy5.png deleted file mode 100644 index 43b01a9a..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy5.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy5@2x.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy5@2x.png deleted file mode 100644 index 353f69e2..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy5@2x.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy6.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy6.png deleted file mode 100644 index 2ae63d20..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy6.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy6@2x.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy6@2x.png deleted file mode 100644 index 5b09faa0..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_greedy6@2x.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy3.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy3.png deleted file mode 100644 index af0482bb..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy3.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy3@2x.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy3@2x.png deleted file mode 100644 index 7d5fd2c6..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy3@2x.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy4.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy4.png deleted file mode 100644 index 920530bd..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy4.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy4@2x.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy4@2x.png deleted file mode 100644 index 1e14c11c..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy4@2x.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy5.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy5.png deleted file mode 100644 index 0a07eeae..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy5.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy5@2x.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy5@2x.png deleted file mode 100644 index 8c9ff19f..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy5@2x.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy6.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy6.png deleted file mode 100644 index 67f0d27a..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy6.png and /dev/null differ diff --git a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy6@2x.png b/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy6@2x.png deleted file mode 100644 index e7eb1aa0..00000000 Binary files a/10-regular-expressions-javascript/6-regexp-greedy-and-lazy/witch_lazy6@2x.png and /dev/null differ diff --git a/10-regular-expressions-javascript/7-regexp-groups/1-find-webcolor-3-or-6/solution.md b/10-regular-expressions-javascript/7-regexp-groups/1-find-webcolor-3-or-6/solution.md deleted file mode 100644 index ece24fb8..00000000 --- a/10-regular-expressions-javascript/7-regexp-groups/1-find-webcolor-3-or-6/solution.md +++ /dev/null @@ -1,30 +0,0 @@ -Регулярное выражение для поиска 3-значного цвета вида `#abc`: /#[a-f0-9]{3}/i. - -Нужно добавить ещё три символа, причём нужны именно три, четыре или семь символов не нужны. Эти три символа либо есть, либо нет. - -Самый простой способ добавить -- просто дописать в конец регэкспа: /#[a-f0-9]{3}([a-f0-9]{3})?/i - -Можно поступить и хитрее: /#([a-f0-9]{3}){1,2}/i. - -Здесь регэксп [a-f0-9]{3} заключён в скобки, чтобы квантификатор {1,2} применялся целиком ко всей этой структуре. - -В действии: -```js -//+ run -var re = /#([a-f0-9]{3}){1,2}/gi; - -var str = "color: #3f3; background-color: #AA00ef; and: #abcd"; - -alert( str.match(re) ); // #3f3 #AA0ef #abc -``` - -В последнем выражении #abcd было найдено совпадение #abc. Чтобы этого не происходило, добавим в конец \b: - -```js -//+ run -var re = /#([a-f0-9]{3}){1,2}\b/gi; - -var str = "color: #3f3; background-color: #AA00ef; and: #abcd"; - -alert( str.match(re) ); // #3f3 #AA0ef -``` diff --git a/10-regular-expressions-javascript/7-regexp-groups/1-find-webcolor-3-or-6/task.md b/10-regular-expressions-javascript/7-regexp-groups/1-find-webcolor-3-or-6/task.md deleted file mode 100644 index 419c0476..00000000 --- a/10-regular-expressions-javascript/7-regexp-groups/1-find-webcolor-3-or-6/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Найдите цвет в формате #abc или #abcdef - -Напишите регулярное выражение, которое находит цвет в формате `#abc` или `#abcdef`. То есть, символ `#`, после которого идут 3 или 6 шестнадцатиричных символа. - -Пример использования: -```js -var re = /* ваш регэксп */ - -var str = "color: #3f3; background-color: #AA00ef; and: #abcd"; - -alert( str.match(re) ); // #3f3 #AA0ef -``` - -P.S. Значения из любого другого количества букв, кроме 3 и 6, такие как `#abcd`, не должны подходить под регэксп. \ No newline at end of file diff --git a/10-regular-expressions-javascript/7-regexp-groups/2-parse-expression/solution.md b/10-regular-expressions-javascript/7-regexp-groups/2-parse-expression/solution.md deleted file mode 100644 index 05fedcd8..00000000 --- a/10-regular-expressions-javascript/7-regexp-groups/2-parse-expression/solution.md +++ /dev/null @@ -1,53 +0,0 @@ -Регулярное выражение для числа, возможно, дробного и отрицательного: -?\d+(\.\d+)?. Мы уже разбирали его в предыдущих задачах. - -Оператор -- это [-+*/]. Заметим, что дефис - идёт в списке первым, так как на любой позиции, кроме первой и последней, он имеет специальный смысл внутри [...], и его понадобилось бы экранировать. - -Кроме того, когда мы оформим это в JavaScript-синтаксис /.../ -- понадобится заэкранировать слэш /. - -Нам нужно число, затем оператор, затем число, и необязательные пробелы между ними. - -Полное регулярное выражение будет таким: -?\d+(\.\d+)?\s*[-+*/]\s*-?\d+(\.\d+)?. - -Чтобы получить результат в виде массива, добавим скобки вокруг тех данных, которые нам интересны, то есть -- вокруг чисел и оператора: (-?\d+(\.\d+)?)\s*([-+*/])\s*(-?\d+(\.\d+)?). - -Посмотрим в действии: -```js -//+ run -var re = /(-?\d+(\.\d+)?)\s*([-+*\/])\s*(-?\d+(\.\d+)?)/; - -alert( "1.2 + 12".match(re) ); -``` - -Итоговый массив будет включать в себя компоненты: - -
      -
    • `result[0] == "1.2 + 12"` (вначале всегда полное совпадение)
    • -
    • `result[1] == "1"` (первая скобка)
    • -
    • `result[2] == "2"` (вторая скобка -- дробная часть `(\.\d+)?`)
    • -
    • `result[3] == "+"` (...)
    • -
    • `result[4] == "12"` (...)
    • -
    • `result[5] == undefined` (последняя скобка, но у второго числа дробная часть отсутствует)
    • -
    - -Нам из этого массива нужны только числа и оператор. А, скажем, дробная часть сама по себе -- не нужна. - -Уберём её из запоминания, добавив в начало скобки ?:, то есть: (?:\.\d+)?. - -Итого, решение: - -```js -//+ run -function parse(expr) { - var re = /(-?\d+(?:\.\d+)?)\s*([-+*\/])\s*(-?\d+(?:\.\d+)?)/; - - var result = expr.match(re); - - if (!result) return; - result.shift(); - - return result; -} - -alert( parse("-1.23 * 3.45") ); // -1.23, *, 3.45 -``` - diff --git a/10-regular-expressions-javascript/7-regexp-groups/2-parse-expression/task.md b/10-regular-expressions-javascript/7-regexp-groups/2-parse-expression/task.md deleted file mode 100644 index c48e5a0c..00000000 --- a/10-regular-expressions-javascript/7-regexp-groups/2-parse-expression/task.md +++ /dev/null @@ -1,20 +0,0 @@ -# Разобрать выражение - -Арифметическое выражение состоит из двух чисел и операции между ними, например: -
      -
    • `1 + 2`
    • -
    • `1.2 * 3.4`
    • -
    • `-3 / -6`
    • -
    • `-2 - 2`
    • -
    - -Список операций: `"+"`, `"-"`, `"*"` и `"/"`. - -Также могут присутсововать пробелы вокруг оператора и чисел. - -Напишите функцию, которая будет получать выражение и возвращать массив из трёх аргументов: -
      -
    1. Первое число.
    2. -
    3. Оператор.
    4. -
    5. Второе число.
    6. -
    diff --git a/10-regular-expressions-javascript/7-regexp-groups/article.md b/10-regular-expressions-javascript/7-regexp-groups/article.md deleted file mode 100644 index 515c6175..00000000 --- a/10-regular-expressions-javascript/7-regexp-groups/article.md +++ /dev/null @@ -1,150 +0,0 @@ -# Скобочные группы - -Часть шаблона может быть заключена в скобки (...). Такие выделенные части шаблона называют "скобочными выражениями" или "скобочными группами". - -У такого выделения есть два эффекта: -
      -
    1. Он позволяет выделить часть совпадения в отдельный элемент массива при поиске через [String#match](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/match) или [RegExp#exec](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec).
    2. -
    3. Если поставить квантификатор после скобки, то он применится *ко всей скобке*, а не всего лишь к одному символу.
    4. -
    - -[cut] - -## Пример - -В примере ниже, шаблон (go)+ находит один или более повторяющихся 'go': - -```js -//+ run -alert( 'Gogogo now!'.match(/(go)+/i ); // "Gogogo" -``` - -Без скобок, шаблон /go+/ означал бы g, после которого идёт одна или более o, например: goooo. А скобки "группируют" (go) вместе. - - -## Содержимое группы - -Скобки нумеруются слева направо. Поисковой движок запоминает содержимое каждой скобки и позволяет обращаться к нему -- в шаблоне и строке замены и, конечно же, в результатах. - -Например, найти HTML-тег можно шаблоном <.*?>. - -После поиска мы захотим что-то сделать с результатом. Для удобства заключим содержимое `<...>` в скобки: <(.*?)>. Тогда оно будет доступно отдельно. - -При поиске методом [String#match](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/match) в результирующем массиве будет сначала всё совпадение, а далее -- скобочные группы. В шаблоне <(.*?)> скобочная группа только одна: - -```js -//+ run -var str = '

    Привет, мир!

    '; -var reg = /<(.*?)>/; - -alert( str.match(reg) ); // массив:

    , h1 -``` - -Заметим, что метод [String#match](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/match) выдаёт скобочные группы только при поиске без флага `/.../g`. В примере выше он нашёл только первое совпадение <h1>, а закрывающий </h1> не нашёл, поскольку без флага `/.../g` ищется только первое совпадение. - -Для того, чтобы искать и с флагом `/.../g` и со скобочными группами, используется метод [RegExp#exec](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec): - -```js -//+ run -var str = '

    Привет, мир!

    '; -var reg = /<(.*?)>/g; - -var match; - -while ((match = reg.exec(str)) !== null) { - // сначала выведет первое совпадение:

    ,h1 - // затем выведет второе совпадение:

    ,/h1 - alert(match); -} -``` - -Теперь найдено оба совпадения <(.*?)>, каждое -- массив из полного совпадения и скобочных групп (одна в данном случае). - -## Вложенные группы -Скобки могут быть и вложенными. В этом случае нумерация также идёт слева направо. - -Например, при поиске тега в <span class="my"> нас может интересовать: - -
      -
    1. Содержимое тега целиком: `span class="my"`.
    2. -
    3. В отдельную переменную для удобства хотелось бы поместить тег: `span`.
    4. -
    5. Также может быть удобно отдельно выделить атрибуты `class="my"`.
    6. -
    - -Добавим скобки в регулярное выражение: - -```js -//+ run -var str = ''; - -reg = /<(([a-z]+)\s*([^>]*))>/; - -alert( str.match(reg) ); // , span, s -``` - -Вот так выглядят скобочные группы: - - - -На нулевом месте -- всегда совпадение полностью, далее -- группы. Нумерация всегда идёт слева направо, по открывающей скобке. - -В данном случае получилось, что группа 1 включает в себя содержимое групп 2 и 3. Это совершенно нормальная ситуация, которая возникает, когда нужно выделить что-то отдельное внутри большей группы. - -**Даже если скобочная группа необязательна и не входит в совпадение, соответствующий элемент массива существует (и равен `undefined`).** - -Например, рассмотрим регэксп a(z)?(c)?. Он ищет `"a"`, за которой не обязательно идёт буква `"z"`, за которой необязательно идёт буква `"c"`. - -Если напустить его на строку из одной буквы `"a"`, то результат будет таков: - -```js -//+ run -match = 'a'.match(/a(z)?(c)?/) - -alert( match.length ); // 3 -alert( match[0] ); // a -alert( match[1] ); // undefined -alert( match[2] ); // undefined -``` - -Массив получился длины `3`, но все скобочные группы -- `undefined`. - -А теперь более сложная ситуация, строка ack: - -```js -//+ run -match = 'ack'.match(/a(z)?(c)?/) - -alert( match.length ); // 3 -alert( match[0] ); // ac, всё совпадение -alert( match[1] ); // undefined, для (z)? ничего нет -alert( match[2] ); // c -``` - -Длина массива результатов по-прежнему `3`. Она постоянна. А вот для скобочной группы (z)? в ней ничего нет, поэтому результат: `["ac", undefined, "c"]`. - -## Исключение из запоминания через ?: - -Бывает так, что скобки нужны, чтобы квантификатор правильно применился, а вот запоминать её в массиве не нужно. - -Скобочную группу можно исключить из запоминаемых и нумеруемых, добавив в её начало ?:. - -Например, мы хотим найти (go)+, но содержимое скобок (`go`) в отдельный элемент массива выделять не хотим. - -Для этого нужно сразу после открывающей скобки поставить `?:`, то есть: (?:go)+. - -Например: - -```js -//+ run -var str = "Gogo John!"; -*!* -var reg = /(?:go)+ (\w+)/i; -*/!* - -var result = str.match(reg); - -alert( result.length ); // 2 -alert( result[1] ); // John -``` - -В примере выше массив результатов имеет длину `2` и содержит только полное совпадение и результат (\w+). Это удобно в тех случаях, когда содержимое скобок нас не интересует. diff --git a/10-regular-expressions-javascript/7-regexp-groups/regexp-nested-groups.png b/10-regular-expressions-javascript/7-regexp-groups/regexp-nested-groups.png deleted file mode 100644 index 9103ea28..00000000 Binary files a/10-regular-expressions-javascript/7-regexp-groups/regexp-nested-groups.png and /dev/null differ diff --git a/10-regular-expressions-javascript/7-regexp-groups/regexp-nested-groups@2x.png b/10-regular-expressions-javascript/7-regexp-groups/regexp-nested-groups@2x.png deleted file mode 100644 index 4f5ee487..00000000 Binary files a/10-regular-expressions-javascript/7-regexp-groups/regexp-nested-groups@2x.png and /dev/null differ diff --git a/10-regular-expressions-javascript/8-regexp-backreferences/1-find-matching-bbtags/solution.md b/10-regular-expressions-javascript/8-regexp-backreferences/1-find-matching-bbtags/solution.md deleted file mode 100644 index 234032d4..00000000 --- a/10-regular-expressions-javascript/8-regexp-backreferences/1-find-matching-bbtags/solution.md +++ /dev/null @@ -1,21 +0,0 @@ - -Открывающий тег -- это \[(b|url|quote)\]. - -Для того, чтобы найти всё до закрывающего -- используем ленивый поиск [\s\S]*? и обратную ссылку на открывающий тег. - -Итого, получится: \[(b|url|quote)\][\s\S]*?\[/\1\]. - -В действии: - -```js -//+ run -var re = /\[(b|url|quote)\][\s\S]*?\[\/\1\]/g; - -var str1 = "..[url]http://ya.ru[/url].."; -var str2 = "..[url][b]http://ya.ru[/b][/url].."; - -alert( str1.match(re) ); // [url]http://ya.ru[/url] -alert( str2.match(re) ); // [url][b]http://ya.ru[/b][/url] -``` - -Для закрывающего тега `[/1]` понадобилось дополнительно экранировать слеш: `\[\/1\]`. diff --git a/10-regular-expressions-javascript/8-regexp-backreferences/1-find-matching-bbtags/task.md b/10-regular-expressions-javascript/8-regexp-backreferences/1-find-matching-bbtags/task.md deleted file mode 100644 index 8596bed5..00000000 --- a/10-regular-expressions-javascript/8-regexp-backreferences/1-find-matching-bbtags/task.md +++ /dev/null @@ -1,41 +0,0 @@ -# Найдите пары тегов - -ББ-тег имеет вид `[имя]...[/имя]`, где имя -- слово, одно из: `b`, `url`, `quote`. - -Например: -``` -[b]текст[/b] -[url]http://ya.ru[/url] -``` - -ББ-теги могут быть вложенными, но сам в себя тег быть вложен не может, например: - -``` -Допустимо: -[url] [b]http://ya.ru[/b] [/url] -[quote] [b]текст[/b] [/quote] - -Нельзя: -[b][b]текст[/b][/b] -``` - -Создайте регулярное выражение для поиска ББ-тегов и их содержимого. - -Например: - -```js -var re = /* регулярка */ - -var str = "..[url]http://ya.ru[/url].."; -alert( str.match(re) ); // [url]http://ya.ru[/url] -``` - -Если теги вложены, то нужно искать самый внешний тег (при желании можно будет продолжить поиск в его содержимом): - -```js -var re = /* регулярка */ - -var str = "..[url][b]http://ya.ru[/b][/url].."; -alert( str.match(re) ); // [url][b]http://ya.ru[/b][/url] -``` - diff --git a/10-regular-expressions-javascript/8-regexp-backreferences/article.md b/10-regular-expressions-javascript/8-regexp-backreferences/article.md deleted file mode 100644 index 3814da12..00000000 --- a/10-regular-expressions-javascript/8-regexp-backreferences/article.md +++ /dev/null @@ -1,68 +0,0 @@ -# Обратные ссылки: \n и $n - -Скобочные группы можно не только получать в результате. - -Движок регулярных выражений запоминает их содержимое, и затем его можно использовать как в самом паттерне, так и в строке замены. - -[cut] - -## Группа в строке замены - -Ссылки в строке замены имеют вид `$n`, где `n` -- это номер скобочной группы. - -Вместо `$n` подставляется содержимое соответствующей скобки: - -```js -//+ run -var name = "Александр Пушкин"; - -name = name.replace(/([а-яё]+) ([а-яё]+)/i, *!*"$2, $1"*/!*); -alert( name ); // Пушкин, Александр -``` - -В примере выше вместо $2 подставляется второе найденное слово, а вместо $1 -- первое. - -## Группа в шаблоне - -Выше был пример использования содержимого групп в строке замены. Это удобно, когда нужно реорганизовать содержимое или создать новое с использованием старого. - -Но к скобочной группе можно также обратиться в самом поисковом шаблоне, ссылкой вида `\номер`. - -Чтобы было яснее, рассмотрим это на реальной задаче -- необходимо найти в тексте строку в кавычках. Причём кавычки могут быть одинарными '...' или двойными "..." -- и то и другое должно искаться корректно. - -Как такие строки искать? - -Можно в регэкспе предусмотреть произвольные кавычки: `['"](.*?)['"]`. Такой регэксп найдёт строки вида "...", '...', но он даст неверный ответ в случае, если одна кавычка ненароком оказалась внутри другой, как например в строке "She's the one!": - -```js -//+ run -str = "He said: \"She's the one!\"."; - -reg = /['"](.*?)['"]/g; - -// Результат не соответствует замыслу -alert( str.match(reg) ); // "She' -``` - -Как видно, регэксп нашёл открывающую кавычку ", затем текст, вплоть до новой кавычки ', которая закрывает соответствие. - -Для того, чтобы попросить регэксп искать закрывающую кавычку -- такую же, как открывающую, мы обернём её в скобочную группу и используем обратную ссылку на неё: - -```js -//+ run -str = "He said: \"She's the one!\"."; - -reg = /(['"])(.*?)\1/g; - -alert( str.match(reg) ); // "She's the one!" -``` - -Теперь работает верно! Движок регулярных выражений, найдя первое скобочное выражение -- кавычку (['"]), запоминает его и далее \1 означает "найти то же самое, что в первой скобочной группе". - -Обратим внимание на два нюанса: - -
      -
    • Чтобы использовать скобочную группу в строке замены -- нужно использовать ссылку вида `$1`, а в шаблоне -- обратный слэш: `\1`.
    • -
    • Чтобы в принципе иметь возможность обратиться к скобочной группе -- не важно откуда, она не должна быть исключена из запоминаемых при помощи `?:`. Скобочные группы вида `(?:...)` не участвуют в нумерации.
    • -
    - diff --git a/10-regular-expressions-javascript/9-regexp-alternation/1-find-programming-language/solution.md b/10-regular-expressions-javascript/9-regexp-alternation/1-find-programming-language/solution.md deleted file mode 100644 index 483f818e..00000000 --- a/10-regular-expressions-javascript/9-regexp-alternation/1-find-programming-language/solution.md +++ /dev/null @@ -1,36 +0,0 @@ -Сначала неправильный способ. - -Если перечислить языки один за другим через `|`, то получится совсем не то: - -```js -//+ run -var reg = /Java|JavaScript|PHP|C|C\+\+/g; - -var str = "Java, JavaScript, PHP, C, C++"; - -alert( str.match(reg) ); // Java,Java,PHP,C,C -``` - -Как видно, движок регулярных выражений ищет альтернации в порядке их перечисления. То есть, он сначала смотрит, есть ли Java, а если нет -- ищет JavaScript. - -Естественно, при этом JavaScript не будет найдено никогда. - -То же самое -- с языками C и C++. - -Есть два решения проблемы: - -
      -
    1. Поменять порядок, чтобы более длинное совпадение проверялось первым: JavaScript|Java|C\+\+|C|PHP.
    2. -
    3. Соединить длинный вариант с коротким: Java(Script)?|C(\+\+)?|PHP.
    4. -
    - -В действии: - -```js -//+ run -var reg = /Java(Script)?|C(\+\+)?|PHP/g; - -var str = "Java, JavaScript, PHP, C, C++"; - -alert( str.match(reg) ); // Java,JavaScript,PHP,C,C++ -``` diff --git a/10-regular-expressions-javascript/9-regexp-alternation/1-find-programming-language/task.md b/10-regular-expressions-javascript/9-regexp-alternation/1-find-programming-language/task.md deleted file mode 100644 index b93570f3..00000000 --- a/10-regular-expressions-javascript/9-regexp-alternation/1-find-programming-language/task.md +++ /dev/null @@ -1,6 +0,0 @@ -# Найдите языки программирования - -Существует много языков программирования, например Java, JavaScript, PHP, C, C++. - -Напишите регулярное выражение, которое найдёт их все в строке "Java JavaScript PHP C++ C" - diff --git a/10-regular-expressions-javascript/9-regexp-alternation/2-match-quoted-string/solution.md b/10-regular-expressions-javascript/9-regexp-alternation/2-match-quoted-string/solution.md deleted file mode 100644 index c959b6fa..00000000 --- a/10-regular-expressions-javascript/9-regexp-alternation/2-match-quoted-string/solution.md +++ /dev/null @@ -1,19 +0,0 @@ -Решение задачи: /"(\\.|[^"\\])*"/g. - -То есть: -
      -
    • Сначала ищем кавычку "
    • -
    • Затем, если далее слэш \\ (удвоение слэша -- техническое, для вставки в регэксп, на самом деле там один слэш), то после него также подойдёт любой символ (точка).
    • -
    • Если не слэш, то берём любой символ, кроме кавычек (которые будут означать конец строки) и слэша (чтобы предотвратить одинокие слэши, сам по себе единственный слэш не нужен, он должен экранировать какой-то символ) [^"\\]
    • -
    • ...И так жадно, до закрывающей кавычки.
    • -
    - -В действии: - -```js -//+ run -var re = /"(\\.|[^"\\])*"/g; -var str = '.. "test me" .. "Скажи \\"Привет\\"!" .. "\\r\\n\\\\" ..'; - -alert( str.match(re) ); // "test me","Скажи \"Привет\"!","\r\n\\" -``` \ No newline at end of file diff --git a/10-regular-expressions-javascript/9-regexp-alternation/2-match-quoted-string/task.md b/10-regular-expressions-javascript/9-regexp-alternation/2-match-quoted-string/task.md deleted file mode 100644 index 4db27891..00000000 --- a/10-regular-expressions-javascript/9-regexp-alternation/2-match-quoted-string/task.md +++ /dev/null @@ -1,26 +0,0 @@ -# Найдите строки в кавычках - -Найдите в тексте при помощи регэкспа строки в двойных кавычках "...". - -В строке поддерживается экранирование при помощи слеша -- примерно в таком же виде, как в обычных строках JavaScript. То есть, строка может содержать любые символы, экранированные слэшем, в частности: \", \n, и даже сам слэш в экранированном виде: \\. - -Здесь особо важно, что двойная кавычка после слэша не оканчивает строку, а считается её частью. В этом и состоит основная сложность задачи, которая без этого условия была бы элементарной. - -Пример совпадающих строк: -```js -.. *!*"test me"*/!* .. (обычная строка) -.. *!*"Скажи \"Привет\"!"*/!* ... (строка с кавычками внутри) -.. *!*"\r\n\\"*/!* .. (строка со спец. символами и слэшем внутри) -``` - -Заметим, что в JavaScript такие строки удобнее всего задавать в одинарных кавычках, и слеши придётся удвоить (в одинарных кавычках они являются экранирующими символами): - -Пример задания тестовой строки в JavaScript: -```js -//+ run -var str = ' .. "test me" .. "Скажи \\"Привет\\"!" .. "\\r\\n\\\\" .. '; - -// эта строка будет такой: -alert(str); // .. "test me" .. "Скажи \"Привет\"!" .. "\r\n\\" .. -``` - diff --git a/10-regular-expressions-javascript/9-regexp-alternation/3-match-exact-tag/solution.md b/10-regular-expressions-javascript/9-regexp-alternation/3-match-exact-tag/solution.md deleted file mode 100644 index 0a422af1..00000000 --- a/10-regular-expressions-javascript/9-regexp-alternation/3-match-exact-tag/solution.md +++ /dev/null @@ -1,18 +0,0 @@ - -Начало шаблона очевидно: ``pattern``, так как ``match`` удовлетворяет этому регэкспу. - -Нужно уточнить его. После ``match`|\s.*?>)`. - -В действии: - -```js -//+ run -var re = /|\s.*?>)/g; - -alert( " diff --git a/2-ui/1-document/02-dom-nodes/inspect.png b/2-ui/1-document/02-dom-nodes/inspect.png new file mode 100644 index 00000000..075cf930 Binary files /dev/null and b/2-ui/1-document/02-dom-nodes/inspect.png differ diff --git a/2-ui/1-document/02-dom-nodes/inspect@2x.png b/2-ui/1-document/02-dom-nodes/inspect@2x.png new file mode 100644 index 00000000..8743dd29 Binary files /dev/null and b/2-ui/1-document/02-dom-nodes/inspect@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/statusbarButtonGlyphs.svg b/2-ui/1-document/02-dom-nodes/statusbarButtonGlyphs.svg similarity index 100% rename from 1-js/3-writing-js/1-debugging-chrome/statusbarButtonGlyphs.svg rename to 2-ui/1-document/02-dom-nodes/statusbarButtonGlyphs.svg diff --git a/2-ui/1-document/02-dom-nodes/toolbarButtonGlyphs.svg b/2-ui/1-document/02-dom-nodes/toolbarButtonGlyphs.svg new file mode 100644 index 00000000..5bdf20a8 --- /dev/null +++ b/2-ui/1-document/02-dom-nodes/toolbarButtonGlyphs.svg @@ -0,0 +1,1035 @@ + +image/svg+xml \ No newline at end of file diff --git a/2-ui/1-document/03-dom-navigation/1-dom-children/solution.md b/2-ui/1-document/03-dom-navigation/1-dom-children/solution.md new file mode 100644 index 00000000..decfa62c --- /dev/null +++ b/2-ui/1-document/03-dom-navigation/1-dom-children/solution.md @@ -0,0 +1,27 @@ +There are many ways, for instance: + + +The `
    ` DOM node: + +```js +document.body.firstElementChild +// or +document.body.children[0] +// or (the first node is space, so we take 2nd) +document.body.childNodes[1] +``` + +The `