Ilya Kantor - The Modern JavaScript Tutorial - Part I. The JavaScript Language 1 (2019)
Ilya Kantor - The Modern JavaScript Tutorial - Part I. The JavaScript Language 1 (2019)
The JavaScript
language
Ilya Kantor
Built at December 1, 2019
The last version of the tutorial is at https://javascript.info.
We constantly work to improve the tutorial. If you find any mistakes, please write at our github.
● An introduction
● An Introduction to JavaScript
●
Manuals and specifications
●
Code editors
● Developer console
● JavaScript Fundamentals
●
Hello, world!
●
Code structure
●
The modern mode, "use strict"
● Variables
● Data types
●
Type Conversions
●
Operators
● Comparisons
●
Interaction: alert, prompt, confirm
●
Conditional operators: if, '?'
● Logical operators
●
Loops: while and for
●
The "switch" statement
● Functions
●
Function expressions
●
Arrow functions, the basics
●
JavaScript specials
●
Code quality
●
Debugging in Chrome
●
Coding Style
● Comments
●
Ninja code
●
Automated testing with Mocha
●
Polyfills
●
Objects: the basics
●
Objects
●
Garbage collection
●
Symbol type
● Object methods, "this"
●
Object to primitive conversion
●
Constructor, operator "new"
●
Data types
●
Methods of primitives
●
Numbers
●
Strings
●
Arrays
●
Array methods
● Iterables
●
Map and Set
●
WeakMap and WeakSet
●
Object.keys, values, entries
●
Destructuring assignment
●
Date and time
● JSON methods, toJSON
●
Advanced working with functions
●
Recursion and stack
●
Rest parameters and spread operator
●
Closure
● The old "var"
●
Global object
●
Function object, NFE
●
The "new Function" syntax
●
Scheduling: setTimeout and setInterval
●
Decorators and forwarding, call/apply
●
Function binding
●
Arrow functions revisited
●
Object properties configuration
● Property flags and descriptors
●
Property getters and setters
●
Prototypes, inheritance
●
Prototypal inheritance
●
F.prototype
●
Native prototypes
●
Prototype methods, objects without __proto__
●
Classes
● Class basic syntax
●
Class inheritance
●
Static properties and methods
●
Private and protected properties and methods
●
Extending built-in classes
●
Class checking: "instanceof"
●
Mixins
●
Error handling
●
Error handling, "try..catch"
●
Custom errors, extending Error
●
Promises, async/await
●
Introduction: callbacks
●
Promise
● Promises chaining
●
Error handling with promises
●
Promise API
●
Promisification
●
Microtasks
●
Async/await
●
Generators, advanced iteration
●
Generators
●
Async iterators and generators
●
Modules
●
Modules, introduction
●
Export and Import
●
Dynamic imports
●
Miscellaneous
●
Proxy and Reflect
●
Eval: run a code string
●
Currying
● BigInt
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.
An introduction
About the JavaScript language and the environment to develop with it.
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?
But as it evolved, JavaScript became a fully independent language with its own specification
called ECMAScript , and now it has no relation to Java at all.
Today, JavaScript can execute not only in the browser, but also on the server, or actually on any
device that has a special program called the JavaScript engine .
The browser has an embedded engine sometimes called a “JavaScript virtual machine”.
Different engines have different “codenames”. For example:
● V8 – in Chrome and Opera.
●
SpiderMonkey – in Firefox.
● …There are other codenames like “Trident” and “Chakra” for different versions of IE,
“ChakraCore” for Microsoft Edge, “Nitro” and “SquirrelFish” for Safari, etc.
The terms above are good to remember because they are used in developer articles on the
internet. We’ll use them too. For instance, if “a feature X is supported by V8”, then it probably
works in Chrome and Opera.
How do engines work?
Engines are complicated. But the basics are easy.
1. The engine (embedded if it’s a browser) reads (“parses”) the script.
2. Then it converts (“compiles”) the script to the machine language.
3. And then the machine code runs, pretty fast.
The engine applies optimizations at each step of the process. It even watches the compiled
script as it runs, analyzes the data that flows through it, and further optimizes the machine
code based on that knowledge.
Modern JavaScript is a “safe” programming language. It does not provide low-level access to
memory or CPU, because it was initially created for browsers which do not require it.
JavaScript’s capabilities greatly depend on the environment it’s running in. For instance,
Node.js supports functions that allow JavaScript to read/write arbitrary files, perform network
requests, etc.
In-browser JavaScript can do everything related to webpage manipulation, interaction with the
user, and the webserver.
JavaScript’s abilities in the browser are limited for the sake of the user’s safety. The aim is to
prevent an evil webpage from accessing private information or harming the user’s data.
There are ways to interact with camera/microphone and other devices, but they require a
user’s explicit permission. So a JavaScript-enabled page may not sneakily enable a web-
camera, observe the surroundings and send the information to the NSA .
● Different tabs/windows generally do not know about each other. Sometimes they do, for
example when one window uses JavaScript to open the other one. But even in this case,
JavaScript from one page may not access the other if they come from different sites (from a
different domain, protocol or port).
This is called the “Same Origin Policy”. To work around that, both pages must agree for data
exchange and contain a special JavaScript code that handles it. We’ll cover that in the tutorial.
This limitation is, again, for the user’s safety. A page from http://anysite.com which a
user has opened must not be able to access another browser tab with the URL
http://gmail.com and steal information from there.
●
JavaScript can easily communicate over the net to the server where the current page came
from. But its ability to receive data from other sites/domains is crippled. Though possible, it
requires explicit agreement (expressed in HTTP headers) from the remote side. Once again,
that’s a safety limitation.
https://javascript.info
https://javascript.info https://gmail.com
<script>
...
</script>
Such limits do not exist if JavaScript is used outside of the browser, for example on a server.
Modern browsers also allow plugin/extensions which may ask for extended permissions.
●
Full integration with HTML/CSS.
● Simple things are done simply.
● Support by all major browsers and enabled by default.
JavaScript is the only browser technology that combines these three things.
That’s what makes JavaScript unique. That’s why it’s the most widespread tool for creating
browser interfaces.
That said, JavaScript also allows to create servers, mobile applications, etc.
The syntax of JavaScript does not suit everyone’s needs. Different people want different features.
That’s to be expected, because projects and requirements are different for everyone.
So recently a plethora of new languages appeared, which are transpiled (converted) to
JavaScript before they run in the browser.
Modern tools make the transpilation very fast and transparent, actually allowing developers to
code in another language and auto-converting it “under the hood”.
Examples of such languages:
● CoffeeScript is a “syntactic sugar” for JavaScript. It introduces shorter syntax, allowing us to
write clearer and more precise code. Usually, Ruby devs like it.
● TypeScript is concentrated on adding “strict data typing” to simplify the development and
support of complex systems. It is developed by Microsoft.
●
Flow also adds data typing, but in a different way. Developed by Facebook.
● Dart is a standalone language that has its own engine that runs in non-browser
environments (like mobile apps), but also can be transpiled to JavaScript. Developed by
Google.
There are more. Of course, even if we use one of transpiled languages, we should also know
JavaScript to really understand what we’re doing.
Summary
● JavaScript was initially created as a browser-only language, but is now used in many other
environments as well.
● Today, JavaScript has a unique position as the most widely-adopted browser language with full
integration with HTML/CSS.
● 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.
Specification
The ECMA-262 specification contains the most in-depth, detailed and formalized information
about JavaScript. It defines the language.
But being that formalized, it’s difficult to understand at first. So if you need the most trustworthy
source of information about the language details, the specification is the right place. But it’s not
for everyday use.
A new specification version is released every year. In-between these releases, the latest
specification draft is at https://tc39.es/ecma262/ .
To read about new bleeding-edge features, including those that are “almost standard” (so-called
“stage 3”), see proposals at https://github.com/tc39/proposals .
Also, if you’re in developing for the browser, then there are other specs covered in the second
part of the tutorial.
Manuals
●
MDN (Mozilla) JavaScript Reference is a manual with examples and other information. It’s
great to get in-depth information about individual language functions, methods etc.
One can find it at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference .
Although, it’s often best to use an internet search instead. Just use “MDN [term]” in the query,
e.g. https://google.com/search?q=MDN+parseInt to search for parseInt function.
● MSDN – Microsoft manual with a lot of information, including JavaScript (often referred to as
JScript). If one needs something specific to Internet Explorer, better go there:
http://msdn.microsoft.com/ .
Also, we can use an internet search with phrases such as “RegExp MSDN” or “RegExp MSDN
jscript”.
Compatibility tables
All these resources are useful in real-life development, as they contain valuable information about
language details, their support etc.
Please remember them (or this page) for the cases when you need in-depth information about a
particular feature.
Code editors
A code editor is the place where programmers spend most of their time.
There are two main types of code editors: IDEs and lightweight editors. Many people use one tool
of each type.
IDE
The term IDE (Integrated Development Environment) refers to a powerful editor with many
features that usually operates on a “whole project.” As the name suggests, it’s not just an editor,
but a full-scale “development environment.”
An IDE loads the project (which can be many files), allows navigation between files, provides
autocompletion based on the whole project (not just the open file), and integrates with a version
management system (like git ), a testing environment, and other “project-level” stuff.
For Windows, there’s also “Visual Studio”, not to be confused with “Visual Studio Code”. “Visual
Studio” is a paid and mighty Windows-only editor, well-suited for the .NET platform. It’s also good
at JavaScript. There’s also a free version Visual Studio Community .
Many IDEs are paid, but have a trial period. Their cost is usually negligible compared to a
qualified developer’s salary, so just choose the best one for you.
Lightweight editors
“Lightweight editors” are not as powerful as IDEs, but they’re fast, elegant and simple.
They are mainly used to open and edit a file instantly.
The main difference between a “lightweight editor” and an “IDE” is that an IDE works on a project-
level, so it loads much more data on start, analyzes the project structure if needed and so on. A
lightweight editor is much faster if we need only one file.
In practice, lightweight editors may have a lot of plugins including directory-level syntax analyzers
and autocompleters, so there’s no strict border between a lightweight editor and an IDE.
The following options deserve your attention:
●
Atom (cross-platform, free).
●
Visual Studio Code (cross-platform, free).
● Sublime Text (cross-platform, shareware).
● Notepad++ (Windows, free).
● Vim and Emacs are also cool if you know how to use them.
The editors in the lists above are those that either I or my friends whom I consider good
developers have been using for a long time and are happy with.
There are other great editors in our big world. Please choose the one you like the most.
The choice of an editor, like any other tool, is individual and depends on your projects, habits, and
personal preferences.
Developer console
Code is prone to errors. You will quite likely make errors… Oh, what am I talking about? You are
absolutely going to make errors, at least if you’re a human, not a robot .
But in the browser, users don’t see errors by default. So, if something goes wrong in the script,
we won’t see what’s broken and can’t fix it.
To see errors and get a lot of other useful information about scripts, “developer tools” have been
embedded in browsers.
Most developers lean towards Chrome or Firefox for development because those browsers have
the best developer tools. Other browsers also provide developer tools, sometimes with special
features, but are usually playing “catch-up” to Chrome or Firefox. So most developers have a
“favorite” browser and switch to others if a problem is browser-specific.
Developer tools are potent; they have many features. To start, we’ll learn how to open them, look
at errors, and run JavaScript commands.
Google Chrome
The exact look of developer tools depends on your version of Chrome. It changes from time to
time but should be similar.
●
Here we can see the red-colored error message. In this case, the script contains an unknown
“lalala” command.
●
On the right, there is a clickable link to the source bug.html:12 with the line number where
the error has occurred.
Below the error message, there is a blue > symbol. It marks a “command line” where we can
type JavaScript commands. Press Enter to run them.
Now we can see errors, and that’s enough for a start. We’ll come back to developer tools later
and cover debugging more in-depth in the chapter Debugging in Chrome.
Multi-line input
Usually, when we put a line of code into the console, and then press Enter , it executes.
To insert multiple lines, press Shift+Enter . This way one can enter long fragments of
JavaScript code.
The look & feel of them is quite similar. Once you know how to use one of these tools (you can
start with Chrome), you can easily switch to another.
Safari
Safari (Mac browser, not supported by Windows/Linux) is a little bit special here. We need to
enable the “Develop menu” first.
Open Preferences and go to the “Advanced” pane. There’s a checkbox at the bottom:
Now 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 F12 for most browsers on Windows. Chrome for Mac needs
Cmd+Opt+J , Safari: Cmd+Opt+C (need to enable first).
Now we have the environment ready. In the next section, we’ll get down to JavaScript.
JavaScript Fundamentals
Let’s learn the fundamentals of script building.
Hello, world!
This part of the tutorial is about core JavaScript, the language itself.
But we need a working environment to run our scripts and, since this book is online, the browser
is a good choice. We’ll keep the amount of browser-specific commands (like alert ) to a
minimum so that you don’t spend time on them if you plan to concentrate on another environment
(like Node.js). We’ll focus on JavaScript in the browser in the next part of the tutorial.
So first, let’s see how we attach a script to a webpage. For server-side environments (like
Node.js), you can execute the script with a command like "node my.js" .
JavaScript programs can be inserted into any part of an HTML document with the help of the
<script> tag.
For instance:
<!DOCTYPE HTML>
<html>
<body>
<script>
alert( 'Hello, world!' );
</script>
</body>
</html>
The <script> tag contains JavaScript code which is automatically executed when the browser
processes the tag.
Modern markup
The <script> tag has a few attributes that are rarely used nowadays but can still be found in
old code:
<script type="text/javascript"><!--
...
//--></script>
This trick isn’t used in modern JavaScript. These comments hide JavaScript code from old
browsers that didn’t know how to process the <script> tag. Since browsers released in the
last 15 years don’t have this issue, this kind of comment can help you identify really old code.
External scripts
<script src="/path/to/script.js"></script>
Here, /path/to/script.js is an absolute path to the script from the site root. One can also
provide a relative path from the current page. For instance, src="script.js" would mean a
file "script.js" in the current folder.
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="/js/script1.js"></script>
<script src="/js/script2.js"></script>
…
Please note:
As a rule, only the simplest scripts are put into HTML. More complex ones reside in separate
files.
The benefit of a separate file is that the browser will download it and store it in its cache .
Other pages that reference the same script will take it from the cache instead of downloading
it, so the file is actually downloaded only once.
That reduces traffic and makes pages faster.
A single <script> tag can’t have both the src attribute and code inside.
<script src="file.js">
alert(1); // the content is ignored, because src is set
</script>
<script src="file.js"></script>
<script>
alert(1);
</script>
Summary
●
We can use a <script> tag to add JavaScript code to a page.
● The type and language attributes are not required.
● A script in an external file can be inserted with <script src="path/to/script.js">
</script> .
There is much more to learn about browser scripts and their interaction with the webpage. But
let’s keep in mind that this part of the tutorial is devoted to the JavaScript language, so we
shouldn’t distract ourselves with browser-specific implementations of it. We’ll be using the
browser as a way to run JavaScript, which is very convenient for online reading, but only one of
many.
✔ Tasks
Show an alert
importance: 5
Do it in a sandbox, or on your hard drive, doesn’t matter, just ensure that it works.
To solution
Take the solution of the previous task Show an alert. Modify it by extracting the script content into
an external file alert.js , residing in the same folder.
To solution
Code structure
The first thing we’ll study is the building blocks of code.
Statements
alert('Hello'); alert('World');
Usually, statements are written on separate lines to make the code more readable:
alert('Hello');
alert('World');
Semicolons
Here, JavaScript interprets the line break as an “implicit” semicolon. This is called an 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:
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”, so the semicolon is
not 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.
An example of an error
If you’re curious to see a concrete example of such an error, check this code out:
[1, 2].forEach(alert)
No need to think about the meaning of the brackets [] and forEach yet. We’ll study them
later. For now, just remember the result of the code: it shows 1 then 2 .
Now, let’s add an alert before the code and not finish it with a semicolon:
[1, 2].forEach(alert)
Now if we run the code, only the first alert is shown and then we have an error!
[1, 2].forEach(alert)
The error in the no-semicolon variant occurs because JavaScript does not assume a
semicolon before square brackets [...] .
So, because the semicolon is not auto-inserted, the code in the first example is treated as a
single statement. Here’s how the engine sees it:
But it should be two separate statements, not one. Such a merging in this case is just wrong,
hence the error. This can happen in other situations.
We recommend putting semicolons between statements even if they are separated by newlines.
This rule is widely adopted by the community. Let’s note once again – it is possible to leave out
semicolons most of the time. But it’s safer – especially for a beginner – to use them.
Comments
As time goes on, programs become more and more complex. It becomes necessary to add
comments which describe what the code does and why.
Comments can be put into any place of a script. They don’t affect its execution because the
engine simply ignores them.
One-line comments start with 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:
Multiline comments start with a forward slash and an asterisk /* and end with an asterisk
and a forward slash */ .
Like this:
Use hotkeys!
In most editors, a line of code can be commented out by pressing the Ctrl+/ hotkey for a
single-line comment and something like Ctrl+Shift+/ – for multiline comments (select a
piece of code and press the hotkey). For Mac, try Cmd instead of Ctrl .
/*
/* nested comment ?!? */
*/
alert( 'World' );
“use strict”
The directive looks like a string: "use strict" or 'use strict' . When it is located at the
top of a script, the whole script works the “modern” way.
For example:
"use strict";
We will learn functions (a way to group commands) soon. Looking ahead, let’s note that "use
strict" can be put at the beginning of the function body instead of the whole script. Doing that
enables strict mode in that function only. But usually, people use it for the whole script.
⚠ Ensure that “use strict” is at the top
Please make sure that "use strict" is at the top of your scripts, otherwise strict mode
may not be enabled.
Strict mode isn’t enabled here:
alert("some code");
// "use strict" below is ignored--it must be at the top
"use strict";
There is no directive like "no use strict" that reverts the engine to old behavior.
Browser console
For the future, when you use a browser console to test features, please note that it doesn’t use
strict by default.
Sometimes, when use strict makes a difference, you’ll get incorrect results.
You can try to press Shift+Enter to input multiple lines, and put use strict on top, like
this:
(function() {
'use strict';
// ...your code...
})()
We have yet to cover the differences between strict mode and the “default” mode.
In the next chapters, as we learn language features, we’ll note the differences between the strict
and default modes. Luckily, there aren’t many and they actually make our lives better.
For now, it’s enough to know about it in general:
1. The "use strict" directive switches the engine to the “modern” mode, changing the
behavior of some built-in features. We’ll see the details later in the tutorial.
2. Strict mode is enabled by placing "use strict" at the top of a script or function. Several
language features, like “classes” and “modules”, enable strict mode automatically.
3. Strict mode is supported by all modern browsers.
4. We recommended always starting scripts with "use strict" . All examples in this tutorial
assume strict mode unless (very rarely) specified otherwise.
Variables
Most of the time, a JavaScript application needs to work with information. Here are two examples:
1. An online shop – the information might include goods being sold and a shopping cart.
2. A chat application – the information might include users, messages, and much more.
A variable
A variable is a “named storage” for data. We can use variables to store goodies, visitors, and
other data.
To create a variable in JavaScript, use the let keyword.
The statement below creates (in other words: declares) a variable with the name “message”:
let message;
Now, we can put some data into it by using the assignment operator = :
let message;
The string is now saved into the memory area associated with the variable. We can access it
using the variable name:
let message;
message = 'Hello!';
To be concise, we can combine the variable declaration and assignment into a single line:
let message = 'Hello!'; // define the variable and assign the value
alert(message); // Hello!
That might seem shorter, but we don’t recommend it. For the sake of better readability, please
use a single line per variable.
Technically, all these variants do the same thing. So, it’s a matter of personal taste and
aesthetics.
The var keyword is almost the same as let . It also declares a variable, but in a slightly
different, “old-school” way.
There are subtle differences between let and var , but they do not matter for us yet. We’ll
cover them in detail in the chapter The old "var".
A real-life analogy
We can easily grasp the concept of a “variable” if we imagine it as a “box” for data, with a
uniquely-named sticker on it.
For instance, the variable message can be imagined as a box labeled "message" with the
value "Hello!" in it:
"H
ell
o
!"
message
let message;
message = 'Hello!';
alert(message);
When the value is changed, the old data is removed from the variable:
llo!"
" He
"W
or
ld
!"
message
We can also declare two variables and copy data from one into the other.
let message;
In such languages, once the value is stored “in the box”, it’s there forever. If we need to store
something else, the language forces us to create a new box (declare a new variable). We
can’t reuse the old one.
Though it may seem a little odd at first sight, these languages are quite capable of serious
development. More than that, there are areas like parallel computations where this limitation
confers certain benefits. Studying such a language (even if you’re not planning to use it soon)
is recommended to broaden the mind.
Variable naming
1. The name must contain only letters, digits, or the symbols $ and _ .
2. The first character must not be a digit.
let userName;
let test123;
When the name contains multiple words, camelCase is commonly used. That is: words go one
after another, each word except first starting with a capital letter: myVeryLongName .
What’s interesting – the dollar sign '$' and the underscore '_' can also be used in names.
They are regular symbols, just like letters, without any special meaning.
alert($ + _); // 3
Case matters
Variables named apple and AppLE are two different variables.
Non-Latin letters are allowed, but not recommended
It is possible to use any language, including cyrillic letters or even hieroglyphs, like this:
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.
⚠ 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: let , class , return , and function are reserved.
alert(num); // 5
"use strict";
Constants
Variables declared using const are called “constants”. They cannot be reassigned. An attempt
to do so would cause an error:
When a programmer is sure that a variable will never change, they can declare it with const to
guarantee and clearly communicate that fact to everyone.
Uppercase constants
There is a widespread practice to use constants as aliases for difficult-to-remember values that
are known prior to execution.
Benefits:
●
COLOR_ORANGE is much easier to remember than "#FF7F00" .
● It is much easier to mistype "#FF7F00" than COLOR_ORANGE .
● When reading the code, COLOR_ORANGE is much more meaningful than #FF7F00 .
When should we use capitals for a constant and when should we name it normally? Let’s make
that clear.
Being a “constant” just means that a variable’s value never changes. But there are constants that
are known prior to execution (like a hexadecimal value for red) and there are constants that are
calculated in run-time, during the execution, but do not change after their initial assignment.
For instance:
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
A variable name should have a clean, obvious meaning, describing the data that it stores.
Variable naming is one of the most important and complex skills in programming. A quick glance
at variable names can reveal which code was written by a beginner versus an experienced
developer.
In a real project, most of the time is spent modifying and extending an existing code base rather
than writing something completely separate from scratch. When we return to some code after
doing something else for a while, it’s much easier to find information that is well-labeled. Or, in
other words, when the variables have good names.
Please spend time thinking about the right name for a variable before declaring it. Doing so will
repay you handsomely.
Some good-to-follow rules are:
● Use human-readable names like userName or shoppingCart .
● Stay away from abbreviations or short names like a , b , c , unless you really know what
you’re doing.
● Make names maximally descriptive and concise. Examples of bad names are data and
value . Such names say nothing. It’s only okay to use them if the context of the code makes
it exceptionally obvious which data or value the variable is referencing.
● Agree on terms within your team and in your own mind. If a site visitor is called a “user” then
we should name related variables currentUser or newUser instead of
currentVisitor or newManInTown .
Sounds simple? Indeed it is, but creating descriptive and concise variable names in practice is
not. Go for it.
Reuse or create?
And the last note. There are some lazy programmers who, instead of declaring new variables,
tend to reuse existing ones.
As a result, their variables are like boxes into which people throw different things without
changing their stickers. What’s inside the box now? Who knows? We need to come closer
and check.
Such programmers save a little bit on variable declaration but lose ten times more on
debugging.
An extra variable is good, not evil.
Modern JavaScript minifiers and browsers optimize code well enough, so it won’t create
performance issues. Using different variables for different values can even help the engine
optimize your code.
Summary
We can declare variables to store data by using the var , let , or const keywords.
● let – is a modern variable declaration.
● var – is an old-school variable declaration. Normally we don’t use it at all, but we’ll cover
subtle differences from let in the chapter The old "var", just in case you need them.
● const – is like let , but the value of the variable can’t be changed.
Variables should be named in a way that allows us to easily understand what’s inside them.
✔ Tasks
To solution
1. Create a variable with the name of our planet. How would you name such a variable?
2. Create a variable to store the name of a current visitor to a website. How would you name
that variable?
To solution
Uppercase const?
importance: 4
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?
Data types
A variable in JavaScript can contain any data. A variable can at one moment be a string and at
another be a number:
// no error
let message = "hello";
message = 123456;
Programming languages that allow such things are called “dynamically typed”, meaning that there
are data types, but variables are not bound to any of them.
There are eight basic data types in JavaScript. Here, we’ll cover them in general and in the next
chapters we’ll talk about each of them in detail.
Number
let n = 123;
n = 12.345;
The number type represents both integer and floating point numbers.
There are many operations for numbers, e.g. multiplication * , division / , addition + ,
subtraction - , and so on.
Besides regular numbers, there are so-called “special numeric values” which also belong to this
data type: Infinity , -Infinity and NaN .
● Infinity represents the mathematical Infinity ∞. It is a special value that’s greater than
any number.
alert( 1 / 0 ); // Infinity
●
NaN represents a computational error. It is a result of an incorrect or an undefined
mathematical operation, for instance:
Special numeric values formally belong to the “number” type. Of course they are not numbers in
the common sense of this word.
We’ll see more about working with numbers in the chapter Numbers.
BigInt
In JavaScript, the “number” type cannot represent integer values larger than 253 (or less than
-253 for negatives), that’s a technical limitation caused by their internal representation. That’s
about 16 decimal digits, so for most purposes the limitation isn’t a problem, but sometimes we
need really big numbers, e.g. for cryptography or microsecond-precision timestamps.
BigInt type was recently added to the language to represent integers of arbitrary length.
As BigInt numbers are rarely needed, we devoted them a separate chapter BigInt.
Compatability issues
Right now BigInt is supported in Firefox and Chrome, but not in Safari/IE/Edge.
String
Double and single quotes are “simple” quotes. There’s practically no difference between them in
JavaScript.
Backticks are “extended functionality” quotes. They allow us to embed variables and expressions
into a string by wrapping them in ${…} , for example:
// 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 in there: a variable like name or an arithmetical expression like 1 + 2 or something
more complex.
Please note that this can only be done in backticks. Other quotes don’t have this embedding
functionality!
alert( "the result is ${1 + 2}" ); // the result is ${1 + 2} (double quotes do nothing)
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 “no, incorrect”.
For instance:
The special null value does not belong to any of the types described above.
It forms a separate type of its own which contains only the null value:
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 represents “nothing”, “empty” or “value unknown”.
The code above states that age is unknown or empty for some reason.
The special value undefined also stands apart. It makes a type of its own, just like null .
let x;
let x = 123;
x = undefined;
alert(x); // "undefined"
…But we don’t recommend doing that. Normally, we use null to assign an “empty” or
“unknown” value to a variable, and we use undefined for checks like seeing if a variable has
been assigned.
All other types are called “primitive” because their values can contain only a single thing (be it a
string or a number or whatever). In contrast, objects are used to store collections of data and
more complex entities. We’ll deal with them later in the chapter Objects after we learn more about
primitives.
The symbol type is used to create unique identifiers for objects. We mention it here for
completeness, but we’ll study it after objects.
The typeof operator returns the type of the argument. It’s useful when we want to process
values of different types differently or just want to do a quick check.
In other words, it works with parentheses or without them. The result is the same.
The call to typeof x returns a string with the type name:
typeof 0 // "number"
1. Math is a built-in object that provides mathematical operations. We will learn it in the chapter
Numbers. Here, it serves just as an example of an object.
2. The result of typeof null is "object" . That’s wrong. It is an officially recognized error in
typeof , kept for compatibility. Of course, null is not an object. It is a special value with a
separate type of its own. So, again, this is an error in the language.
3. The result of typeof alert is "function" , because alert is a function. We’ll study
functions in the next chapters where we’ll also see that there’s no special “function” type in
JavaScript. Functions belong to the object type. But typeof treats them differently, returning
"function" . That’s not quite correct, but very convenient in practice.
Summary
number for numbers of any kind: integer or floating-point, integers are limited by ±253.
●
In the next chapters, we’ll concentrate on primitive values and once we’re familiar with them, we’ll
move on to objects.
✔ Tasks
String quotes
importance: 5
To solution
Type Conversions
Most of the time, operators and functions automatically convert the values given to them to the
right type.
For example, alert automatically converts any value to a string to show it. Mathematical
operations convert values to numbers.
There are also cases when we need to explicitly convert a value to the expected type.
String Conversion
String conversion is mostly obvious. A false becomes "false" , null becomes "null" ,
etc.
Numeric Conversion
Explicit conversion is usually required when we read a value from a string-based source like a
text form but expect a number to be entered.
If the string is not a valid number, the result of such a conversion is NaN . For instance:
Value Becomes…
undefined NaN
null 0
Whitespaces from the start and end are removed. If the remaining string is empty, the result is 0 .
string
Otherwise, the number is “read” from the string. An error gives NaN .
Examples:
Please note that null and undefined behave differently here: null becomes zero while
undefined becomes NaN .
This only happens when at least one of the arguments is a string. Otherwise, values are
converted to numbers.
Boolean Conversion
It happens in logical operations (later we’ll meet condition tests and other similar things) but can
also be performed explicitly with a call to Boolean(value) .
Some languages (namely PHP) treat "0" as false . But in JavaScript, a non-empty string
is always true .
Summary
The three most widely used type conversions are to string, to number, and to boolean.
Value Becomes…
undefined NaN
null 0
true / false 1 / 0
The string is read “as is”, whitespaces from both sides are ignored. An empty string becomes 0 . An error
string
gives NaN .
Value Becomes…
Most of these rules are easy to understand and memorize. The notable exceptions where people
usually make mistakes are:
● undefined is NaN as a number, not 0 .
● "0" and space-only strings like " " are true as a boolean.
Objects aren’t covered here. We’ll return to them later in the chapter Object to primitive
conversion that is devoted exclusively to objects after we learn more basic things about
JavaScript.
✔ Tasks
Type conversions
importance: 5
"" + 1 + 0
"" - 1 + 0
true + false
6 / "3"
"2" * "3"
4 + 5 + "px"
"$" + 4 + 5
"4" - 2
"4px" - 2
7 / 0
" -9 " + 5
" -9 " - 5
null + 1
undefined + 1
" \t \n" - 2
Think well, write down and then compare with the answer.
To solution
Operators
We know many operators from school. They are things like addition + , multiplication * ,
subtraction - , and so on.
In this chapter, we’ll concentrate on aspects of operators that are not covered by school
arithmetic.
let x = 1;
x = -x;
alert( x ); // -1, unary negation was applied
● An operator is binary if it has two operands. The same minus exists in binary form as well:
let x = 1, y = 3;
alert( y - x ); // 2, binary minus subtracts values
Formally, in the examples above we have two different operators that share the same symbol:
the negation operator, a unary operator that reverses the sign, and the subtraction operator, a
binary operator that subtracts one number from another.
Now, let’s see special features of JavaScript operators that are beyond school arithmetics.
Usually, the plus operator + sums numbers.
Note that if one of the operands is a string, the other one is converted to a string too.
For example:
See, it doesn’t matter whether the first operand is a string or the second one. The rule is simple: if
either operand is a string, the other one is converted into a string as well.
However, note that operations run from left to right. If there are two numbers followed by a string,
the numbers will be added before being converted to a string:
String concatenation and conversion is a special feature of the binary plus + . Other arithmetic
operators work only with numbers and always convert their operands to numbers.
The plus + exists in two forms: the binary form that we used above and the unary form.
The unary plus or, in other words, the plus operator + applied to a single value, doesn’t do
anything to numbers. But if the operand is not a number, the unary plus converts it into a number.
For example:
// No effect on numbers
let x = 1;
alert( +x ); // 1
let y = -2;
alert( +y ); // -2
// Converts non-numbers
alert( +true ); // 1
alert( +"" ); // 0
The need to convert strings to numbers arises very often. For example, if we are getting values
from HTML form fields, they are usually strings. What if we want to sum them?
If we want to treat them as numbers, we need to convert and then sum them:
From a mathematician’s standpoint, the abundance of pluses may seem strange. But from a
programmer’s standpoint, there’s nothing special: unary pluses are applied first, they convert
strings to numbers, and then the binary plus sums them up.
Why are unary pluses applied to values before the binary ones? As we’re going to see, that’s
because of their higher precedence.
Operator precedence
If an expression has more than one operator, the execution order is defined by their precedence,
or, in other words, the default priority order of operators.
From school, we all know that the multiplication in the expression 1 + 2 * 2 should be
calculated before the addition. That’s exactly the precedence thing. The multiplication is said to
have a higher precedence than the addition.
Parentheses override any precedence, so if we’re not satisfied with the default order, we can use
them to change it. For example, write (1 + 2) * 2 .
There are many operators in JavaScript. Every operator has a corresponding precedence
number. The one with the larger number executes first. If the precedence is the same, the
execution order is from left to right.
Here’s an extract from the precedence table (you don’t need to remember this, but note that
unary operators are higher than corresponding binary ones):
… … …
16 unary plus +
16 unary negation -
14 multiplication *
14 division /
13 addition +
13 subtraction -
… … …
3 assignment =
… … …
As we can see, the “unary plus” has a priority of 16 which is higher than the 13 of “addition”
(binary plus). That’s why, in the expression "+apples + +oranges" , unary pluses work
before the addition.
Assignment
Let’s note that an assignment = is also an operator. It is listed in the precedence table with the
very low priority of 3 .
That’s why, when we assign a variable, like x = 2 * 2 + 1 , the calculations are done first
and then the = is evaluated, storing the result in x .
let x = 2 * 2 + 1;
alert( x ); // 5
It is possible to chain assignments:
let a, b, c;
a = b = c = 2 + 2;
alert( a ); // 4
alert( b ); // 4
alert( c ); // 4
Chained assignments evaluate from right to left. First, the rightmost expression 2 + 2 is
evaluated and then assigned to the variables on the left: c , b and a . At the end, all the
variables share a single value.
The call x = value writes the value into x and then returns it.
let a = 1;
let b = 2;
let c = 3 - (a = b + 1);
alert( a ); // 3
alert( c ); // 0
In the example above, the result of expression (a = b + 1) is the value which was
assigned to a (that is 3 ). It is then used for further evaluations.
Funny code, isn’t it? We should understand how it works, because sometimes we see it in
JavaScript libraries, but shouldn’t write anything like that ourselves. Such tricks definitely
don’t make code clearer or readable.
Remainder %
For instance:
For instance:
alert( 2 ** 2 ); // 4 (2 * 2)
alert( 2 ** 3 ); // 8 (2 * 2 * 2)
alert( 2 ** 4 ); // 16 (2 * 2 * 2 * 2)
For instance:
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 it:
● Increment ++ increases a variable by 1:
let counter = 2;
counter++; // works the same as counter = counter + 1, but is shorter
alert( counter ); // 3
let counter = 2;
counter--; // works the same as counter = counter - 1, but is shorter
alert( counter ); // 1
⚠ Important:
Increment/decrement can only be applied to variables. Trying to use it on a value like 5++
will give an error.
Let’s clarify. As we know, all operators return a value. Increment/decrement is no exception. The
prefix form returns the new value while the postfix form returns the old value (prior to
increment/decrement).
To see the difference, here’s an example:
let counter = 1;
let a = ++counter; // (*)
alert(a); // 2
In the line (*) , the prefix form ++counter increments counter and returns the new value,
2 . So, the alert shows 2 .
let counter = 1;
let a = counter++; // (*) changed ++counter to counter++
alert(a); // 1
In the line (*) , the postfix form counter++ also increments counter but returns the old
value (prior to increment). So, the alert shows 1 .
To summarize:
● If the result of increment/decrement is not used, there is no difference in which form to use:
let counter = 0;
counter++;
++counter;
alert( counter ); // 2, the lines above did the same
● If we’d like to increase a value and immediately use the result of the operator, we need the
prefix form:
let counter = 0;
alert( ++counter ); // 1
● If we’d like to increment a value but use its previous value, we need the postfix form:
let counter = 0;
alert( counter++ ); // 0
Increment/decrement among other operators
The operators ++/-- can be used inside expressions as well. Their precedence is higher
than most other arithmetical operations.
For instance:
let counter = 1;
alert( 2 * ++counter ); // 4
Compare with:
let counter = 1;
alert( 2 * counter++ ); // 2, because counter++ returns the "old" value
Though technically okay, such notation usually makes code less readable. One line does
multiple things – not good.
While reading code, a fast “vertical” eye-scan can easily miss something like counter++
and it won’t be obvious that the variable increased.
We advise a style of “one line – one action”:
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 need to delve into low-level
number representation and it would not be optimal to do that right now, especially since we won’t
need them any time soon. If you’re curious, you can read the Bitwise Operators article on
MDN. It would be more practical to do that when a real need arises.
Modify-in-place
We often need to apply an operator to a variable and store the new result in that same variable.
For example:
let n = 2;
n = n + 5;
n = n * 2;
let n = 2;
n += 5; // now n = 7 (same as n = n + 5)
n *= 2; // now n = 14 (same as n = n * 2)
alert( n ); // 14
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:
let n = 2;
n *= 3 + 5;
Comma
The comma operator , is one of the rarest and most unusual operators. Sometimes, it’s used to
write shorter code, so we need to know it in order to understand what’s going on.
The comma operator allows us to evaluate several expressions, dividing them with a comma , .
Each of them is evaluated but only the result of the last one is returned.
For example:
let a = (1 + 2, 3 + 4);
Here, the first expression 1 + 2 is evaluated and its result is thrown away. Then, 3 + 4 is
evaluated and returned as the result.
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.
Why do we need an operator that throws away everything except the last expression?
Sometimes, people use it in more complex constructs to put several actions in one line.
For example:
Such tricks are used in many JavaScript frameworks. That’s why we’re mentioning them. But
usually they don’t improve code readability so we should think well before using them.
✔ Tasks
What are the final values of all variables a , b , c and d after the code below?
let a = 1, b = 1;
let c = ++a; // ?
let d = b++; // ?
To solution
Assignment result
importance: 3
let a = 2;
let x = 1 + (a *= 2);
To solution
Comparisons
We know many comparison operators from maths:
● Greater/less than: a > b , a < b .
● Greater/less than or equals: a >= b , a <= b .
● Equals: a == b (please note the double equals sign = . A single symbol a = b would
mean an assignment).
● Not equals. In maths the notation is ≠ , but in JavaScript it’s written as an assignment with an
exclamation sign before it: a != b .
Like all other operators, a comparison returns a value. In this case, the value is a boolean.
● true – means “yes”, “correct” or “the truth”.
● false – means “no”, “wrong” or “not the truth”.
For example:
String comparison
To see whether a string is greater than another, JavaScript uses the so-called “dictionary” or
“lexicographical” order.
In other words, strings are compared letter-by-letter.
For example:
In the examples above, the comparison 'Z' > 'A' gets to a result at the first step while the
strings "Glow" and "Glee" are compared character-by-character:
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.
For instance, case matters. A capital letter "A" is not equal to the lowercase "a" . Which
one is greater? The lowercase "a" . Why? Because the lowercase character has a greater
index in the internal encoding table JavaScript uses (Unicode). We’ll get back to specific
details and consequences of this in the chapter Strings.
When comparing values of different types, JavaScript converts the values to numbers.
For example:
For example:
For example:
let a = 0;
alert( Boolean(a) ); // false
let b = "0";
alert( Boolean(b) ); // true
From JavaScript’s standpoint, this result is quite normal. An equality check converts values
using the numeric conversion (hence "0" becomes 0 ), while the explicit Boolean
conversion uses another set of rules.
Strict equality
This happens because operands of different types are converted to numbers by the equality
operator == . An empty string, just like false , becomes a zero.
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.
There’s a non-intuitive behavior when null or undefined are compared to other values.
Now let’s see some funny things that happen when we apply these rules. And, what’s more
important, how to not fall into a trap with them.
Mathematically, that’s strange. The last result states that " null is greater than or equal to zero",
so in one of the comparisons above it must be true , but they are both false.
The reason is that an equality check == and comparisons > < >= <= work differently.
Comparisons convert null to a number, treating it as 0 . That’s why (3) null >= 0 is true
and (1) null > 0 is false.
On the other hand, the equality check == for undefined and null is defined such that,
without any conversions, they equal each other and don’t equal anything else. That’s why (2)
null == 0 is false.
An incomparable undefined
The value undefined shouldn’t be compared to other values:
alert( undefined > 0 ); // false (1)
alert( undefined < 0 ); // false (2)
alert( undefined == 0 ); // false (3)
Evade problems
Why did we go over these examples? Should we remember these peculiarities all the time? Well,
not really. Actually, these tricky things will gradually become familiar over time, but there’s a solid
way to evade problems with them:
Just treat any comparison with undefined/null except the strict equality === with
exceptional care.
Don’t use comparisons >= > < <= with a variable which may be null/undefined , unless
you’re really sure of what you’re doing. If a variable can have these values, check for them
separately.
Summary
● Comparison operators return a boolean value.
● Strings are compared letter-by-letter in the “dictionary” order.
● When values of different types are compared, they get converted to numbers (with the
exclusion of a strict equality check).
●
The values null and undefined equal == each other and do not equal any other value.
● Be careful when using comparisons like > or < with variables that can occasionally be
null/undefined . Checking for null/undefined separately is a good idea.
✔ Tasks
Comparisons
importance: 5
5 > 4
"apple" > "pineapple"
"2" > "12"
undefined == null
undefined === null
null == "\n0\n"
null === +"\n0\n"
To solution
alert
Syntax:
alert(message);
This shows a message and pauses script execution until the user presses “OK”.
For example:
alert("Hello");
The mini-window with the message is called a modal window. The word “modal” means that the
visitor can’t interact with the rest of the page, press other buttons, etc. until they have dealt with
the window. In this case – until they press “OK”.
prompt
It shows a modal window with a text message, an input field for the visitor, and the buttons
OK/Cancel.
title
The text to show the visitor.
default
An optional second parameter, the initial value for the input field.
The visitor may type something in the prompt input field and press OK. Or they can cancel the
input by pressing Cancel or hitting the Esc key.
The call to prompt returns the text from the input field or null if the input was canceled.
For instance:
alert(`You are ${age} years old!`); // You are 100 years old!
So, for prompts to look good in IE, we recommend always providing the second argument:
confirm
The syntax:
result = confirm(question);
The function confirm shows a modal window with a question and two buttons: OK and
Cancel.
The result is true if OK is pressed and false otherwise.
For example:
Summary
alert
shows a message.
prompt
shows a message asking the user to input text. It returns the text or, if Cancel button or Esc is
clicked, null .
confirm
shows a message and waits for the user to press “OK” or “Cancel”. It returns true for OK and
false for Cancel/ Esc .
All these methods are modal: they pause script execution and don’t allow the visitor to interact
with the rest of the page until the window has been dismissed.
There are two limitations shared by all the methods above:
1. The exact location of the modal window is determined by the browser. Usually, it’s in the
center.
2. The exact look of the window also depends on the browser. We can’t modify it.
That is the price for simplicity. There are other ways to show nicer windows and richer interaction
with the visitor, but if “bells and whistles” do not matter much, these methods work just fine.
✔ Tasks
A simple page
importance: 4
To solution
The if(...) statement evaluates a condition in parentheses and, if the result is true ,
executes a block of code.
For example:
let year = prompt('In which year was ECMAScript-2015 specification published?', '');
if (year == 2015) {
alert( "That's correct!" );
alert( "You're so smart!" );
}
We recommend wrapping your code block with curly braces {} every time you use an if
statement, even if there is only one statement to execute. Doing so improves readability.
Boolean conversion
The if (…) statement evaluates the expression in its parentheses and converts the result to a
boolean.
Let’s recall the conversion rules from the chapter Type Conversions:
● A number 0 , an empty string "" , null , undefined , and NaN all become false .
Because of that they are called “falsy” values.
● Other values become true , so they are called “truthy”.
if (0) { // 0 is falsy
...
}
if (1) { // 1 is truthy
...
}
if (cond) {
...
}
The if statement may contain an optional “else” block. It executes when the condition is false.
For example:
let year = prompt('In which year was the ECMAScript-2015 specification published?', '');
if (year == 2015) {
alert( 'You guessed it right!' );
} else {
alert( 'How can you be so wrong?' ); // any value except 2015
}
Sometimes, we’d like to test several variants of a condition. The else if clause lets us do that.
For example:
let year = prompt('In which year was the ECMAScript-2015 specification published?', '');
In the code above, JavaScript first checks year < 2015 . If that is falsy, it goes to the next
condition year > 2015 . If that is also falsy, it shows the last alert .
let accessAllowed;
let age = prompt('How old are you?', '');
alert(accessAllowed);
The so-called “conditional” or “question mark” operator lets us do that in a shorter and simpler
way.
The operator is represented by a question mark ? . Sometimes it’s called “ternary”, because the
operator has three operands. It is actually the one and only operator in JavaScript which has that
many.
The syntax is:
The condition is evaluated: if it’s truthy then value1 is returned, otherwise – value2 .
For example:
Technically, we can omit the parentheses around age > 18 . The question mark operator has a
low precedence, so it executes after the comparison > .
But parentheses make the code more readable, so we recommend using them.
Please note:
In the example above, you can avoid using the question mark operator because the
comparison itself returns true/false :
// the same
let accessAllowed = age > 18;
Multiple ‘?’
A sequence of question mark operators ? can return a value that depends on more than one
condition.
For instance:
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:
if (age < 3) {
message = 'Hi, baby!';
} else if (age < 18) {
message = 'Hello!';
} else if (age < 100) {
message = 'Greetings!';
} else {
message = 'What an unusual age!';
}
(company == 'Netscape') ?
alert('Right!') : alert('Wrong.');
Depending on the condition company == 'Netscape' , either the first or the second
expression after the ? gets executed and shows an alert.
We don’t assign a result to a variable here. Instead, we execute different code depending on the
condition.
It’s not recommended to use the question mark operator in this way.
The notation is shorter than the equivalent if statement, which appeals to some programmers.
But it is less readable.
Our eyes scan the code vertically. Code blocks which span several lines are easier to understand
than a long, horizontal instruction set.
The purpose of the question mark operator ? is to return one value or another depending on its
condition. Please use it for exactly that. Use if when you need to execute different branches of
code.
✔ Tasks
if ("0") {
alert( 'Hello' );
}
To solution
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!”
Begin
What's the
“official” name of
JavaScript?
Other ECMAScript
Using if..else , write the code which gets a number via prompt and then shows in alert :
To solution
let result;
if (a + b < 4) {
result = 'Below';
} else {
result = 'Over';
}
To solution
For readability, it’s recommended to split the code into multiple lines.
let message;
if (login == 'Employee') {
message = 'Hello';
} else if (login == 'Director') {
message = 'Greetings';
} else if (login == '') {
message = 'No login';
} else {
message = '';
}
To solution
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.
Their result can also be of any type.
Let’s see the details.
|| (OR)
result = a || b;
In classical programming, the logical OR is meant to manipulate boolean values only. If any of its
arguments are true , it returns true , otherwise it returns false .
In JavaScript, the operator is a little bit trickier and more powerful. But first, let’s see what
happens with boolean values.
As we can see, the result is always true except for the case when both operands are false .
Most of the time, OR || is used in an if statement to test if any of the given conditions is
true .
For example:
let hour = 9;
The logic described above is somewhat classical. Now, let’s bring in the “extra” features of
JavaScript.
The extended algorithm works as follows.
Given multiple OR’ed values:
alert( 1 || 0 ); // 1 (1 is truthy)
alert( true || 'no matter what' ); // (true is truthy)
This leads to some interesting usage compared to a “pure, classical, boolean-only OR”.
If both currentUser and defaultUser were falsy, "unnamed" would be the result.
2. Short-circuit evaluation.
Operands can be not only values, but arbitrary expressions. OR evaluates and tests them from
left to right. The evaluation stops when a truthy value is reached, and the value is returned.
This process is called “a short-circuit evaluation” because it goes as short as possible from left
to right.
This is clearly seen when the expression given as the second argument has a side effect like a
variable assignment.
In the example below, x does not get assigned:
let x;
true || (x = 1);
If, instead, the first argument is false , || evaluates the second one, thus running the
assignment:
let x;
false || (x = 1);
alert(x); // 1
An assignment is a simple case. There may be side effects, that won’t show up if the
evaluation doesn’t reach them.
As we can see, such a use case is a "shorter way of doing if ". The first operand is converted
to boolean. If it’s false, the second one is evaluated.
Most of time, it’s better to use a “regular” if to keep the code easy to understand, but
sometimes this can be handy.
&& (AND)
In classical programming, AND returns true if both operands are truthy and false otherwise:
An example with if :
In other words, AND returns the first falsy value or the last value if none were 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:
We can also pass several values in a row. See how the first falsy one is returned:
So the code a && b || c && d is essentially the same as if the && expressions were in
parentheses: (a && b) || (c && d) .
Just like OR, the AND && operator can sometimes replace if .
For instance:
let x = 1;
The action in the right part of && would execute only if the evaluation reaches it. That is, only if
(x > 0) is true.
let x = 1;
if (x > 0) {
alert( 'Greater than zero!' );
}
The variant with && appears shorter. But if is more obvious and tends to be a little bit more
readable.
So we recommend using every construct for its purpose: use if if we want if and use && if we
want AND.
! (NOT)
The boolean NOT operator is represented with an exclamation sign ! .
result = !value;
For instance:
That is, the first NOT converts the value to boolean and returns the inverse, and the second NOT
inverses it again. In the end, we have a plain value-to-boolean conversion.
There’s a little more verbose way to do the same thing – a built-in Boolean function:
The precedence of NOT ! is the highest of all logical operators, so it always executes first,
before && or || .
✔ Tasks
To solution
To solution
To solution
To solution
To solution
To solution
Create two variants: the first one using NOT ! , the second one – without it.
To solution
To solution
If the visitor enters "Admin" , then prompt for a password, if the input is an empty line or Esc
– show “Canceled”, if it’s another string – then show “I don’t know you”.
The schema:
Begin
Who's there?
Password?
Please use nested if blocks. Mind the overall readability of the code.
Hint: passing an empty input to a prompt returns an empty string '' . Pressing ESC during a
prompt returns null .
To solution
For example, outputting goods from a list one after another or just running the same code for
each number from 1 to 10.
Loops are a way to repeat the same code multiple times.
while (condition) {
// code
// so-called "loop body"
}
While the condition is truthy, the code from the loop body is executed.
For instance, the loop below outputs i while i < 3 :
let i = 0;
while (i < 3) { // shows 0, then 1, then 2
alert( i );
i++;
}
A single execution of the loop body is called an iteration. The loop in the example above makes
three iterations.
If i++ was missing from the example above, the loop would repeat (in theory) forever. In
practice, the browser provides ways to stop such loops, and in server-side JavaScript, we can kill
the process.
Any expression or variable can be a loop condition, not just comparisons: the condition is
evaluated and converted to a boolean by while .
let i = 3;
while (i) { // when i becomes 0, the condition becomes falsy, and the loop stops
alert( i );
i--;
}
let i = 3;
while (i) alert(i--);
The condition check can be moved below the loop body using the do..while syntax:
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:
let i = 0;
do {
alert( i );
i++;
} while (i < 3);
This form of syntax should only be used when you want the body of the loop to execute at least
once regardless of the condition being truthy. Usually, the other form is preferred: while(…)
{…} .
The for loop is more complex, but it’s also the most commonly used loop.
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 :
part
condition i < 3 Checked before every loop iteration. If false, the loop stops.
body alert(i) Runs again and again while the condition is truthy.
Run begin
→ (if condition → run body and run step)
→ (if condition → run body and run step)
→ (if condition → run body and run step)
→ ...
That is, begin executes once, and then it iterates: after each condition test, body and
step are executed.
If you are new to loops, it could help to go back to the example and reproduce how it runs step-
by-step on a piece of paper.
Here’s exactly what happens in our case:
// 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
let i = 0;
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:
let i = 0;
for (;;) {
// repeats without limits
}
Please note that the two for semicolons ; must be present. Otherwise, there would be a
syntax error.
But we can force the exit at any time using the special break directive.
For example, the loop below asks the user for a series of numbers, “breaking” when no number is
entered:
let sum = 0;
while (true) {
sum += value;
}
alert( 'Sum: ' + sum );
The break directive is activated at the line (*) if the user enters an empty line or cancels the
input. It stops the loop immediately, passing control to the first line after the loop. Namely,
alert .
The combination “infinite loop + break as needed” is great for situations when a loop’s
condition must be checked not in the beginning or end of the loop, but in the middle or even in
several places of its body.
We can use it if we’re done with the current iteration and would like to move on to the next one.
The loop below uses continue to output only odd values:
alert(i); // 1, then 3, 5, 7, 9
}
For even values of i , the continue directive stops executing the body and passes control to
the next iteration of for (with the next number). So the alert is only called for odd values.
if (i % 2) {
alert( i );
}
From a technical point of view, this is identical to the example above. Surely, we can just wrap
the code in an if block instead of using continue .
But as a side-effect, this created one more level of nesting (the alert call inside the curly
braces). If the code inside of if is longer than a few lines, that may decrease the overall
readability.
⚠ No break/continue to the right side of ‘?’
Please note that syntax constructs that are not expressions cannot be used with the ternary
operator ? . In particular, directives such as break/continue aren’t allowed there.
if (i > 5) {
alert(i);
} else {
continue;
}
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:
In the code above, break outer looks upwards for the label named outer and breaks out of
that loop.
outer:
for (let i = 0; i < 3; i++) { ... }
The continue directive can also be used with a label. In this case, code execution jumps to the
next iteration of the labeled loop.
A call to break/continue is only possible from inside a loop and the label must be
somewhere above the directive.
Summary
If we don’t want to do anything in the current iteration and would like to forward to the next one,
we can use the continue directive.
break/continue support labels before the loop. A label is the only way for
break/continue to escape a nested loop to go to an outer one.
✔ Tasks
let i = 3;
while (i) {
alert( i-- );
}
To solution
For every loop iteration, write down which value it outputs and then compare it with the solution.
1.
let i = 0;
while (++i < 5) alert( i );
2.
let i = 0;
while (i++ < 5) alert( i );
To solution
1.
2.
To solution
To solution
Rewrite the code changing the for loop to while without altering its behavior (the output
should stay same).
To solution
Write a loop which prompts for a number greater than 100 . If the visitor enters another number –
ask them 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.
To solution
An integer number greater than 1 is called a prime 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 .
Write the code which outputs prime numbers in the interval from 2 to n .
P.S. The code should work for any n , not be hard-tuned for any fixed value.
To solution
The syntax
The switch has one or more case blocks and an optional default.
switch(x) {
case 'value1': // if (x === 'value1')
...
[break]
default:
...
[break]
}
● The value of x is checked for a strict equality to the value from the first case (that is,
value1 ) then to the second ( value2 ) and so on.
● If the equality is found, switch starts to execute the code starting from the corresponding
case , until the nearest break (or until the end of switch ).
● If no case is matched then the default code is executed (if it exists).
An example
let a = 2 + 2;
switch (a) {
case 3:
alert( 'Too small' );
break;
case 4:
alert( 'Exactly!' );
break;
case 5:
alert( 'Too large' );
break;
default:
alert( "I don't know such values" );
}
Here the switch starts to compare a from the first case variant that is 3 . The match fails.
Then 4 . That’s a match, so the execution starts from case 4 until the nearest break .
If there is no break then the execution continues with the next case without any
checks.
An example without break :
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" );
}
alert( 'Exactly!' );
alert( 'Too big' );
alert( "I don't know such values" );
For example:
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 :
let a = 2 + 2;
switch (a) {
case 4:
alert('Right!');
break;
default:
alert('The result is strange. Really.');
}
The ability to “group” cases is a side-effect of how switch/case works without break . Here
the execution of case 3 starts from the line (*) and goes through case 5 , because there’s
no break .
Type matters
Let’s emphasize that the equality check is always strict. The values must be of the same type to
match.
For example, let’s consider the code:
case '2':
alert( 'Two' );
break;
case 3:
alert( 'Never executes!' );
break;
default:
alert( 'An unknown value' );
}
✔ Tasks
Write the code using if..else which would correspond to the following switch :
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!' );
}
To solution
if (a == 0) {
alert( 0 );
}
if (a == 1) {
alert( 1 );
}
if (a == 2 || a == 3) {
alert( '2,3' );
}
To solution
Functions
Quite often we need to perform a similar action in many places of the script.
For example, we need to show a nice-looking message when a visitor logs in, logs out and
maybe somewhere else.
Functions are the main “building blocks” of the program. They allow the code to be called many
times without repetition.
We’ve already seen examples of built-in functions, like alert(message) ,
prompt(message, default) and confirm(question) . But we can create functions of
our own as well.
Function Declaration
function showMessage() {
alert( 'Hello everyone!' );
}
The function keyword goes first, then goes the name of the function, then a list of parameters
between the parentheses (comma-separated, empty in the example above) and finally the code
of the function, also named “the function body”, between curly braces.
function name(parameters) {
...body...
}
For instance:
function showMessage() {
alert( 'Hello everyone!' );
}
showMessage();
showMessage();
The call showMessage() executes the code of the function. Here we will see the message two
times.
This example clearly demonstrates one of the main purposes of functions: to avoid code
duplication.
If we ever need to change the message or the way it is shown, it’s enough to modify the code in
one place: the function which outputs it.
Local variables
For example:
function showMessage() {
let message = "Hello, I'm JavaScript!"; // local variable
alert( message );
}
Outer variables
function showMessage() {
let message = 'Hello, ' + userName;
alert(message);
}
For instance:
function showMessage() {
userName = "Bob"; // (1) changed the outer variable
showMessage();
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:
function showMessage() {
let userName = "Bob"; // declare a local variable
alert( userName ); // John, unchanged, the function did not access the outer variable
Global variables
Variables declared outside of any function, such as the outer userName in the code above,
are called global.
Global variables are visible from any function (unless shadowed by locals).
It’s a good practice to minimize the use of global variables. Modern code has few or no
globals. Most variables reside in their functions. Sometimes though, they can be useful to
store project-level data.
Parameters
We can pass arbitrary data to functions using parameters (also called function arguments) .
In the example below, the function has two parameters: from and text .
When the function is called in lines (*) and (**) , the given values are copied to local
variables from and text . Then the function uses them.
Here’s one more example: we have a variable from and pass it to the function. Please note: the
function changes from , but the change is not seen outside, because a function always gets a
copy of the value:
// the value of "from" is the same, the function modified a local copy
alert( from ); // Ann
Default values
For instance, the aforementioned function showMessage(from, text) can be called with a
single argument:
showMessage("Ann");
That’s not an error. Such a call would output "Ann: undefined" . There’s no text , so it’s
assumed that text === undefined .
If we want to use a “default” text in this case, then we can specify it after = :
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:
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:
function sum(a, b) {
return a + b;
}
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).
function checkAge(age) {
if (age > 18) {
return true;
} else {
return confirm('Do you have permission from your parents?');
}
}
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:
function showMovie(age) {
if ( !checkAge(age) ) {
return;
}
In the code above, if checkAge(age) returns false , then showMovie won’t proceed to the
alert .
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 :
function doNothing() {
return;
}
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:
return;
(some + long + expression + or + whatever * f(a) + f(b))
return (
some + long + expression
+ or +
whatever * f(a) + f(b)
)
Naming a function
Functions are actions. So their name is usually a verb. It should be brief, as accurate as possible
and describe what the function does, so that someone reading the code gets an indication of
what the function does.
It is a widespread practice to start a function with a verbal prefix which vaguely describes the
action. There must be an agreement within the team on the meaning of the prefixes.
For instance, functions that start with "show" usually show something.
With prefixes in place, a glance at a function name gives an understanding what kind of work it
does and what kind of value it returns.
Two independent actions usually deserve two functions, even if they are usually called
together (in that case we can make a 3rd function that calls those two).
These examples assume common meanings of prefixes. You and your team are free to agree
on other meanings, but usually they’re not much different. In any case, you should have a firm
understanding of what a prefix means, what a prefixed function can and cannot do. All same-
prefixed functions should obey the rules. And the team should share the knowledge.
Ultrashort function names
Functions that are used very often sometimes have ultrashort names.
For example, the jQuery framework defines a function with $ . The Lodash library has
its core function named _ .
These are exceptions. Generally functions names should be concise and descriptive.
Functions == Comments
Functions should be short and do exactly one thing. If that thing is big, maybe it’s worth it to split
the function into a few smaller functions. Sometimes following this rule may not be that easy, but
it’s definitely a good thing.
A separate function is not only easier to test and debug – its very existence is a great comment!
For instance, compare the two functions showPrimes(n) below. Each one outputs prime
numbers up to n .
function showPrimes(n) {
nextPrime: for (let i = 2; i < n; i++) {
alert( i ); // a prime
}
}
The second variant uses an additional function isPrime(n) to test for primality:
function showPrimes(n) {
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
To make the code clean and easy to understand, it’s recommended to use mainly local variables
and parameters in the function, not outer variables.
It is always easier to understand a function which gets parameters, works with them and returns a
result than a function which gets no parameters, but modifies outer variables as a side-effect.
Function naming:
●
A name should clearly describe what the function does. When we see a function call in the
code, a good name instantly gives us an understanding what it does and returns.
● A function is an action, so function names are usually verbal.
●
There exist many well-known function prefixes like create… , show… , get… , check… and
so on. Use them to hint what a function does.
Functions are the main building blocks of scripts. Now we’ve covered the basics, so we actually
can start creating and using them. But that’s only the beginning of the path. We are going to
return to them many times, going more deeply into their advanced features.
✔ Tasks
Is "else" required?
importance: 4
The following function returns true if the parameter age is greater than 18 .
function checkAge(age) {
if (age > 18) {
return true;
} else {
// ...
return confirm('Did parents allow you?');
}
}
function checkAge(age) {
if (age > 18) {
return true;
}
// ...
return confirm('Did parents allow you?');
}
To solution
The following function returns true if the parameter age is greater than 18 .
function checkAge(age) {
if (age > 18) {
return true;
} else {
return confirm('Did parents allow you?');
}
}
To solution
Function min(a, b)
importance: 1
Write a function min(a,b) which returns the least of two numbers a and b .
For instance:
min(2, 5) == 2
min(3, -1) == -1
min(1, 1) == 1
To solution
Function pow(x,n)
importance: 4
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.
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) .
P.S. In this task the function should support only natural values of n : integers up from 1 .
To solution
Function expressions
In JavaScript, a function is not a “magical language structure”, but a special kind of value.
function sayHi() {
alert( "Hello" );
}
There is another syntax for creating a function that is called a Function Expression.
It looks like this:
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 ".
Please note that the last line does not run the function, because there are no parentheses after
sayHi . There are programming languages where any mention of a function name causes its
execution, but JavaScript is not like that.
In JavaScript, a function is a value, so we can deal with it as a value. The code above shows its
string representation, which is the source code.
Surely, a function is a special value, in the sense that we can call it like sayHi() .
But it’s still a value. So we can work with it like with other kinds of values.
Note that we could also have used a Function Expression to declare sayHi , in the first line:
function sayHi() {
// ...
}
Callback functions
Let’s look at more examples of passing functions as values and using function expressions.
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() :
function showOk() {
alert( "You agreed." );
}
function showCancel() {
alert( "You canceled the execution." );
}
In practice, such functions are quite useful. The major difference between a real-life ask and the
example above is that real-life functions use more complex ways to interact with the user than a
simple confirm . In the browser, such function usually draws a nice-looking question window.
But that’s another story.
The arguments showOk and showCancel of ask are called callback functions or just
callbacks.
The idea is that we pass a function and expect it to be “called back” later if necessary. In our
case, showOk becomes the callback for “yes” answer, and showCancel for “no” answer.
We can use Function Expressions to write the same function much shorter:
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.
Let’s formulate the key differences between Function Declarations and Expressions.
// 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” = :
// Function Expression
let sum = function(a, b) {
return a + b;
};
The more subtle difference is when a function is created by the JavaScript engine.
A Function Expression is created when the execution reaches it and is usable only from
that moment.
Once the execution flow passes to the right side of the assignment let sum = function… –
here we go, the function is created and can be used (assigned, called, etc. ) from now on.
Function Declarations are different.
For example, a global Function Declaration is visible in the whole script, no matter where it is.
That’s due to internal algorithms. When JavaScript prepares to run the script, it first looks for
global Function Declarations in it and creates the functions. We can think of it as an “initialization
stage”.
And after all Function Declarations are processed, the code is executed. So it has access to
these functions.
function sayHi(name) {
alert( `Hello, ${name}` );
}
The Function Declaration sayHi is created when JavaScript is preparing to start the script and
is visible everywhere in it.
…If it were a Function Expression, then it wouldn’t work:
sayHi("John"); // error!
Function Expressions are created when the execution reaches them. That would happen only in
the line (*) . Too late.
Another special feature of Function Declarations is their block scope.
In strict mode, when a Function Declaration is within a code block, it’s visible everywhere
inside that block. But not outside of it.
For instance, let’s imagine that we need to declare a function welcome() depending on the
age variable that we get during runtime. And then we plan to use it some time later.
function welcome() {
alert("Hello!");
}
} else {
function welcome() {
alert("Greetings!");
}
// ...use it later
welcome(); // Error: welcome is not defined
That’s because a Function Declaration is only visible inside the code block in which it resides.
} else {
function welcome() {
alert("Greetings!");
}
}
The correct approach would be to use a Function Expression and assign welcome to the
variable that is declared outside of if and has the proper visibility.
let welcome;
welcome = function() {
alert("Hello!");
};
} else {
welcome = function() {
alert("Greetings!");
};
welcome(); // ok now
welcome(); // ok now
That’s also better for readability, as it’s easier to look up function f(…) {…} in the code
than let f = function(…) {…}; . Function Declarations are more “eye-catching”.
…But if a Function Declaration does not suit us for some reason, or we need a conditional
declaration (we’ve just seen an example), then Function Expression should be used.
Summary
●
Functions are values. They can be assigned, copied or declared in any place of the code.
● If the function is declared as a separate statement in the main code flow, that’s called a
“Function Declaration”.
●
If the function is created as a part of an expression, it’s called a “Function Expression”.
● Function Declarations are processed before the code block is executed. They are visible
everywhere in the block.
●
Function Expressions are created when the execution flow reaches them.
In most cases when we need to declare a function, a Function Declaration is preferable, because
it is visible prior to the declaration itself. That gives us more flexibility in code organization, and is
usually more readable.
So we should use a Function Expression only when a Function Declaration is not fit for the task.
We’ve seen a couple of examples of that in this chapter, and will see more in the future.
…This creates a function func that accepts arguments arg1..argN , then evaluates the
expression on the right side with their use and returns its result.
alert( sum(1, 2) ); // 3
As you can, see (a, b) => a + b means a function that accepts two arguments named a
and b . Upon the execution, it evaluates the expression a + b and returns the result.
●
If we have only one argument, then parentheses around parameters can be omitted, making
that even shorter.
For example:
alert( double(3) ); // 6
●
If there are no arguments, parentheses will be empty (but they should be present):
sayHi();
welcome(); // ok now
Arrow functions may appear unfamiliar and not very readable at first, but that quickly changes as
the eyes get used to the structure.
They are very convenient for simple one-line actions, when we’re just too lazy to write many
words.
The examples above took arguments from the left of => and evaluated the right-side expression
with them.
Sometimes we need something a little bit more complex, like multiple expressions or statements.
It is also possible, but we should enclose them in curly braces. Then use a normal return
within them.
Like this:
let sum = (a, b) => { // the curly brace opens a multiline function
let result = a + b;
return result; // if we use curly braces, then we need an explicit "return"
};
alert( sum(1, 2) ); // 3
More to come
Here we praised arrow functions for brevity. But that’s not all!
To study them in-depth, we first need to get to know some other aspects of JavaScript, so
we’ll return to arrow functions later in the chapter Arrow functions revisited.
For now, we can already use arrow functions for one-line actions and callbacks.
Summary
Arrow functions are handy for one-liners. They come in two flavors:
1. Without curly braces: (...args) => expression – the right side is an expression: the
function evaluates it and returns the result.
2. With curly braces: (...args) => { body } – brackets allow us to write multiple
statements inside the function, but we need an explicit return to return something.
✔ Tasks
ask(
"Do you agree?",
function() { alert("You agreed."); },
function() { alert("You canceled the execution."); }
);
To solution
JavaScript specials
This chapter briefly recaps the features of JavaScript that we’ve learned by now, paying special
attention to subtle moments.
Code structure
alert('Hello')
alert('World')
That’s called “automatic semicolon insertion”. Sometimes it doesn’t work, for instance:
[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:
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: Code structure.
Strict mode
To fully enable all features of modern JavaScript, we should start scripts with "use strict" .
'use strict';
...
The directive must be at the top of a script or at the beginning of a function body.
Without "use strict" , everything still works, but some features behave in the old-fashion,
“compatible” way. We’d generally prefer the modern behavior.
Some modern features of the language (like classes that we’ll study in the future) enable strict
mode implicitly.
let x = 5;
x = "John";
The typeof operator returns the type for a value, with two exceptions:
Interaction
prompt(question, [default])
Ask a question , and return either what the visitor entered or null if they clicked “cancel”.
confirm(question)
Ask a question and suggest to choose between Ok and Cancel. The choice is returned as
true/false .
alert(message)
Output a message .
All these functions are modal, they pause the code execution and prevent the visitor from
interacting with the page until they answer.
For instance:
Operators
Arithmetical
Regular: * + - / , also % for the remainder and ** for power of a number.
The binary plus + concatenates strings. And if any of the operands is a string, the other one is
converted to string too:
Assignments
There is a simple assignment: a = b and combined ones like a *= 2 .
Bitwise
Bitwise operators work with 32-bit integers at the lowest, bit-level: see the docs when they are
needed.
Conditional
The only operator with three parameters: cond ? resultA : resultB . If cond is truthy,
returns resultA , otherwise resultB .
Logical operators
Logical AND && and OR || perform short-circuit evaluation and then return the value where it
stopped (not necessary true / false ). Logical NOT ! converts the operand to boolean type
and returns the inverse value.
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:
Values null and undefined are special: they equal == each other and don’t equal anything
else.
Greater/less comparisons compare strings character-by-character, other types are converted to a
number.
Other operators
There are few others, like a comma operator.
Loops
● We covered 3 types of loops:
// 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.
For instance:
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");
}
Functions
function sum(a, b) {
let result = a + b;
return result;
}
return result;
};
3. Arrow functions:
● 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 .
More to come
That was a brief list of JavaScript features. As of now we’ve studied only basics. Further in the
tutorial you’ll find more specials and advanced features of JavaScript.
Code quality
This chapter explains coding practices that we’ll use further in the development.
Debugging in Chrome
Before writing more complex code, let’s talk about debugging.
Debugging is the process of finding and fixing errors within a script. All modern browsers and
most other environments support debugging tools – a special UI in developer tools that makes
debugging much easier. It also allows to trace the code step by step to see what exactly is going
on.
We’ll be using Chrome here, because it has enough features, most other browsers have a similar
process`.
Your Chrome version may look a little bit different, but it still should be obvious what’s there.
● Open the example page in Chrome.
●
Turn on developer tools with F12 (Mac: Cmd+Opt+I ).
●
Select the Sources panel.
Here’s what you should see if you are doing it for the first time:
open sources
Let’s click it and select hello.js in the tree view. Here’s what should show up:
1 2 3
Here we can see three zones:
1. The Resources zone lists HTML, JavaScript, CSS and other files, including images that are
attached to the page. Chrome extensions may appear here too.
2. The Source zone shows the source code.
3. The Information and control zone is for debugging, we’ll explore it soon.
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 Enter
to execute.
For example, here 1+2 results in 3 , and hello("debugger") returns nothing, so the result
is undefined :
Breakpoints
Let’s examine what’s going on within the code of the example page. In hello.js , click at line
number 4 . Yes, right on the 4 digit, not on the code.
Congratulations! You’ve set a breakpoint. Please also click on the number for line 8 .
A breakpoint is a point of code where the debugger will automatically pause the JavaScript
execution.
While the code is paused, we can examine current variables, execute commands in the console
etc. In other words, we can debug it.
We can always find a list of breakpoints in the right panel. That’s useful when we have many
breakpoints in various files. It allows us to:
● Quickly jump to the breakpoint in the code (by clicking on it in the right panel).
●
Temporarily disable the breakpoint by unchecking it.
● Remove the breakpoint by right-clicking and selecting Remove.
●
…And so on.
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 certain function
parameters.
Debugger command
We can also pause the code by using the debugger command in it, like this:
function hello(name) {
let phrase = `Hello, ${name}!`;
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.
In our example, hello() is called during the page load, so the easiest way to activate the
debugger (after we’ve set the breakpoints) is to reload the page. So let’s press F5 (Windows,
Linux) or Cmd+R (Mac).
watch expressions 1
2
see the outer call details
current variables 3
Please open the informational dropdowns to the right (labeled with arrows). They allow you to
examine the current code state:
1. Watch – shows current values for any expressions.
You can click the plus + and input an expression. The debugger will show its value at any
moment, automatically recalculating it in the process of execution.
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 (e.g. “anonymous”), the debugger jumps to the corresponding code,
and all its variables can be examined as well.
3. Scope – current variables.
Local shows local function variables. You can also see their values highlighted right over the
source.
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.
nested calls
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.
Clicking this again and again will step through all script statements one by one.
– “Step over”: run the next command, but don’t go into a function, hotkey F10 .
Similar to the previous the “Step” command, but behaves differently if the next statement is a
function call. That is: not a built-in, like alert , but a function of our own.
The “Step” command goes into it and pauses the execution at its first line, while “Step over”
executes the nested function call invisibly, skipping the function internals.
That’s good if we’re not interested to see what happens inside the function call.
For the future, just note that “Step” command ignores async actions, such as setTimeout
(scheduled function call), that execute later. The “Step into” goes into their code, waiting for them
if necessary. See DevTools manual for more details.
– “Step out”: continue the execution till the end of the current function, hotkey
Shift+F11 .
Continue the execution and stop it at the very last line of the current function. That’s handy when
we accidentally entered a nested call using , but it does not interest us, and we want to
continue to its end as soon as possible.
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 to the line, but we’re too lazy to
set a breakpoint.
Logging
Regular users don’t see that output, it is in the console. To see it, either open the Console panel
of developer tools or press Esc while in another panel: that opens the console at the bottom.
If we have enough logging in our code, then we can see what’s going on from the records,
without the debugger.
Summary
1. A breakpoint.
2. The debugger statements.
3. An error (if dev tools are open and the button is “on”).
When paused, we can debug – examine variables and trace the code to see where the execution
goes wrong.
There are many more options in developer tools than covered here. The full manual is at
https://developers.google.com/web/tools/chrome-devtools .
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 and context menus!
Coding Style
Our code must be as clean and easy to read as possible.
That is actually the art of programming – to take a complex task and code it in a way that is both
correct and human-readable. A good code style greatly assists in that.
Syntax
Here is a cheat sheet with some suggested rules (see below for more details):
Spaces
around operators
Indentation
2 spaces 2
A space
A semicolon ;
after for/if/while…
is mandatory
A space
between
parameters
Now let’s discuss the rules and reasons for them in detail.
Curly Braces
In most JavaScript projects curly braces are written in “Egyptian” style with the opening brace on
the same line as the corresponding keyword – not on a new line. There should also be a space
before the opening bracket, like this:
if (condition) {
// do this
// ...and that
// ...and that
}
Here are the annotated variants so you can judge their readability for yourself:
1. 😠 Beginners sometimes do that. Bad! Curly braces are not needed:
2. 😠 Split to a separate line without braces. Never do that, easy to make an error when adding
new lines:
if (n < 0)
alert(`Power ${n} is not supported`);
if (n < 0) {
alert(`Power ${n} is not supported`);
}
For a very brief code, one line is allowed, e.g. if (cond) return null . But a code block
(the last variant) is usually more readable.
Line Length
No one likes to read a long horizontal line of code. It’s best practice to split them.
For example:
if (
id === 123 &&
moonPhase === 'Waning Gibbous' &&
zodiacSign === 'Libra'
) {
letTheSorceryBegin();
}
The maximum line length should be agreed upon at the team-level. It’s usually 80 or 120
characters.
Indents
There are two types of indents:
● Horizontal indents: 2 or 4 spaces.
A horizontal indentation is made using either 2 or 4 spaces or the horizontal tab symbol (key
Tab ). Which one to choose is an old holy war. Spaces are more common nowadays.
One advantage of spaces over tabs is that spaces allow more flexible configurations of indents
than the tab symbol.
For instance, we can align the arguments with the opening bracket, like this:
show(parameters,
aligned, // 5 spaces padding at the left
one,
after,
another
) {
// ...
}
●
Vertical indents: empty lines for splitting code into logical blocks.
Even a single function can often be divided into logical blocks. In the example below, the
initialization of variables, the main loop and returning the result are split vertically:
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 nine lines of code without a vertical indentation.
Semicolons
A semicolon should be present after each statement, even if it could possibly be skipped.
There are languages where a semicolon is truly optional and it is rarely used. In JavaScript,
though, there are cases where a line break is not interpreted as a semicolon, leaving the code
vulnerable to errors. See more about that in the chapter Code structure.
If you’re an experienced JavaScript programmer, you may choose a no-semicolon code style like
StandardJS . Otherwise, it’s best to use semicolons to avoid possible pitfalls. The majority of
developers put semicolons.
Nesting Levels
Try to avoid nesting code too many levels deep.
For example, in the loop, it’s sometimes a good idea to use the continue directive to avoid
extra nesting.
We can write:
function pow(x, n) {
if (n < 0) {
alert("Negative 'n' not supported");
} else {
let result = 1;
return result;
}
}
Option 2:
function pow(x, n) {
if (n < 0) {
alert("Negative 'n' not supported");
return;
}
let result = 1;
return result;
}
The second one is more readable because the “special case” of n < 0 is handled early on.
Once the check is done we can move on to the “main” code flow without the need for additional
nesting.
Function Placement
If you are writing several “helper” functions and the code that uses them, there are three ways to
organize the functions.
// function declarations
function createElement() {
...
}
function setHandler(elem) {
...
}
function walkAround() {
...
}
function setHandler(elem) {
...
}
function walkAround() {
...
}
That’s because when reading code, we first want to know what it does. If the code goes first, then
it becomes clear from the start. Then, maybe we won’t need to read the functions at all,
especially if their names are descriptive of what they actually do.
Style Guides
A style guide contains general rules about “how to write” code, e.g. which quotes to use, how
many spaces to indent, the maximal line length, etc. A lot of minor things.
When all members of a team use the same style guide, the code looks uniform, regardless of
which team member wrote it.
Of course, a team can always write their own style guide, but usually there’s no need to. There
are many existing guides to choose from.
●
Airbnb JavaScript Style Guide
●
Idiomatic.JS
●
StandardJS
If you’re a novice developer, start with the cheat sheet at the beginning of this chapter. Then you
can browse other style guides to pick up more ideas and decide which one you like best.
Automated Linters
Linters are tools that can automatically check the style of your code and make improving
suggestions.
The great thing about them is that style-checking can also find some bugs, like typos in variable
or function names. Because of this feature, using a linter is recommended even if you don’t want
to stick to one particular “code style”.
Most linters are integrated with many popular editors: just enable the plugin in the editor and
configure the style.
For instance, for ESLint you should do the following:
1. Install Node.js .
2. Install ESLint with the command npm install -g eslint (npm is a JavaScript package
installer).
3. Create a config file named .eslintrc in the root of your JavaScript project (in the folder that
contains all your files).
4. Install/enable the plugin for your editor that integrates with ESLint. The majority of editors have
one.
{
"extends": "eslint:recommended",
"env": {
"browser": true,
"node": true,
"es6": true
},
"rules": {
"no-console": 0,
"indent": ["warning", 2]
}
}
Here the directive "extends" denotes that the configuration is based on the
“eslint:recommended” set of settings. After that, we specify our own.
It is also possible to download style rule sets from the web and extend them instead. See
http://eslint.org/docs/user-guide/getting-started for more details about installation.
Also certain IDEs have built-in linting, which is convenient but not as customizable as ESLint.
Summary
All syntax rules described in this chapter (and in the style guides referenced) aim to increase the
readability of your code. All of them are debatable.
When we think about writing “better” code, the questions we should ask ourselves are: “What
makes the code more readable and easier to understand?” and “What can help us avoid errors?”
These are the main things to keep in mind when choosing and debating code styles.
Reading popular style guides will allow you to keep up to date with the latest ideas about code
style trends and best practices.
✔ Tasks
Bad style
importance: 4
function pow(x,n)
{
let result=1;
for(let i=0;i<n;i++) {result*=x;}
return result;
}
Fix it.
To solution
Comments
As we know from the chapter Code structure, comments can be single-line: starting with // and
multiline: /* ... */ .
We normally use them to describe how and why the code works.
At first sight, commenting might be obvious, but novices in programming often use them wrongly.
Bad comments
Novices tend to use comments to explain “what is going on in the code”. Like this:
// This code will do this thing (...) and that thing (...)
// ...and who knows what else...
very;
complex;
code;
But in good code, the amount of such “explanatory” comments should be minimal. Seriously, the
code should be easy to understand without them.
There’s a great rule about that: “if the code is so unclear that it requires a comment, then maybe
it should be rewritten instead”.
function showPrimes(n) {
nextPrime:
for (let i = 2; i < n; i++) {
alert(i);
}
}
function showPrimes(n) {
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. The function itself becomes the comment. Such code is
called self-descriptive.
// ...
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();
//...
}
}
Once again, functions themselves tell what’s going on. There’s nothing to comment. 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 avoid “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
So, explanatory comments are usually bad. Which comments are good?
For instance:
/**
* Returns x raised to the n-th power.
*
* @param {number} x The number to raise.
* @param {number} n The power, must be a natural number.
* @return {number} x raised to the n-th power.
*/
function pow(x, n) {
...
}
Such comments allow us to understand the purpose of the function and use it the right way
without looking in its code.
By the way, many editors like WebStorm can understand them as well and use them to
provide autocomplete and some automatic code-checking.
Also, there are tools like JSDoc 3 that can generate HTML-documentation from the
comments. You can read more information about JSDoc at http://usejsdoc.org/ .
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.
Summary
An important sign of a good developer is comments: their presence and even their absence.
Good comments allow us to maintain the code well, come back to it after a delay and use it more
effectively.
Comment this:
● Overall architecture, high-level view.
●
Function usage.
● Important solutions, especially when not immediately obvious.
Avoid comments:
● That tell “how code works” and “what it does”.
● Put them in only if it’s impossible to make the code so simple and self-descriptive that it
doesn’t require them.
Comments are also used for auto-documenting tools like JSDoc3: they read them and generate
HTML-docs (or docs in another format).
Ninja code
Programmer ninjas of the past used these tricks to sharpen the mind of code maintainers.
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?
⚠ Irony detected
Many try to follow ninja paths. Few succeed.
Make the code as short as possible. Show how smart you are.
Let subtle language features guide you.
Cool, right? If you write like that, a developer who comes across this line and tries to understand
what is the value of i is going to have a merry time. Then come to you, seeking for an answer.
Tell them that shorter is always better. Initiate them into the paths of ninja.
One-letter variables
The Dao hides in wordlessness. Only the Dao is Laozi (Tao Te Ching)
well begun and well completed.
Another way to code faster is to use single-letter variable names everywhere. Like a , b or c .
A short variable disappears in the code like a real ninja in the forest. No one will be able to find it
using “search” of the editor. And even if someone does, they won’t be able to “decipher” what the
name a or b means.
…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 many 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, they won’t be able to quickly
figure out that the variable named x is the loop counter.
Use abbreviations
If the team rules forbid the use of one-letter and vague names – shorten them, make
abbreviations.
Like this:
●
list → lst .
● userAgent → ua .
●
browser → brsr .
●
…etc
Only the one with truly good intuition will be able to understand such names. Try to shorten
everything. Only a worthy person should be able to uphold the development of your code.
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 you can. Indeed, every variable
holds data, right?
…But what to do if data is already taken? Try value , it’s also universal. After all, a variable
eventually gets a value.
●
Name a variable by its type: str , num …
Give them a try. A young initiate may wonder – are such names really useful for a ninja?
Indeed, they are!
Sure, the variable name still means something. It says what’s inside the variable: a string, a
number or something else. But when an outsider tries to understand the code, they’ll be
surprised to see that there’s actually no information at all! And will ultimately fail to alter your
well-thought code.
The value type is easy to find out by debugging. But what’s the meaning of the variable?
Which string/number does it store?
There’s just no way to figure out without a good meditation!
●
…But what if there are no more such names? Just add a number: data1, item2,
elem5 …
Attention test
Only a truly attentive programmer should be able to understand your code. But how to check
that?
One of the ways – use similar variable names, like date and data .
Smart synonyms
Using similar names for same things makes life more interesting and shows your creativity to the
public.
For instance, consider function prefixes. If a function shows a message on the screen – start it
with display… , like displayMessage . And then if another function shows on the screen
something else, like a user name, start it with show… (like showName ).
Insinuate that there’s a subtle 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 much more
interesting and diverse the code became.
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 similarly
named function printMessage : “Where does it 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
For instance:
function ninjaFunction(elem) {
// 20 lines of code working with elem
elem = clone(elem);
A fellow programmer who wants to work with elem in the second half of the function will be
surprised… Only during the debugging, after examining the code they will find out that they’re
working with a clone!
Put underscores _ and __ before variable names. Like _name or __value . It would be great
if only you knew their meaning. Or, better, add them just for fun, without particular meaning at all.
Or different meanings in different places.
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.
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 of their paid working time.
Use same names for variables inside and outside a function. As simple. No efforts to invent new
names.
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 fail to notice that there’s a local
user shadowing the outer one.
Then they’ll try to work with user assuming that it’s the external variable, the result of
authenticateUser() … The trap is sprung! Hello, debugger…
Side-effects everywhere!
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. In other words, without “side-effects”.
A really beautiful trick is to add a “useful” action to them, besides the main task.
An expression of dazed surprise on the face of your colleague when they see a function named
is.. , check.. or find... changing something – will definitely broaden your boundaries of
reason.
Another way to surprise is to return a non-standard result.
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!
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 them. So they 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
enlightenment.
When we write a function, we can usually imagine what it should do: which parameters give
which results.
During 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 is wrong – then we fix the code, run again, check the result – and so on till it works.
But such manual “re-runs” are imperfect.
That’s very typical. When we develop something, we keep a lot of possible use cases in mind.
But it’s hard to expect a programmer to check all of them manually after every change. So it
becomes easy to fix one thing and break another one.
Automated testing means that tests are written separately, in addition to the code. They
run our functions in various ways and compare results with the expected.
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 the ** 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 contains descriptions of use
cases together with tests for them, like this:
describe("pow", function() {
});
A spec has three main building blocks that you can see above:
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 add later.
The specification can be executed, and it will run the test specified in it block. We’ll see that
later.
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.
Here in the tutorial we’ll be using the following JavaScript libraries for tests:
●
Mocha – the core framework: it provides common testing functions including describe
and it and the main function that runs tests.
●
Chai – the library with many assertions. It allows to use a lot of different assertions, for now
we need only assert.equal .
● Sinon – 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:
<!DOCTYPE html>
<html>
<head>
<!-- add mocha css, to show results -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css">
<!-- add mocha framework code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script>
<script>
mocha.setup('bdd'); // minimal setup
</script>
<!-- add chai -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script>
<script>
// chai has a lot of stuff, let's make assert global
let assert = chai.assert;
</script>
</head>
<body>
<script>
function pow(x, n) {
/* function code is to be written, empty now */
}
</script>
<!-- the element with id="mocha" will contain test results -->
<div id="mocha"></div>
</html>
The result:
pow
✖ raises to n-th power ‣
As of now, the test fails, there’s an error. That’s logical: we have an empty function code in pow ,
so pow(2,3) returns undefined instead of 8 .
For the future, let’s note that there are more high-level test-runners, like karma and others,
that make it easy to autorun many different tests.
Initial implementation
function pow(x, n) {
return 8; // :) we cheat!
}
pow
✓ raises to n-th power ‣
What we’ve done is definitely a cheat. The function does not work: an attempt to calculate
pow(3,4) would give an incorrect result, but tests pass.
…But the situation is quite typical, it happens in practice. Tests pass, but the function works
wrong. Our spec is imperfect. We need to add more use cases to it.
1. The first variant – add one more assert into the same it :
describe("pow", function() {
});
describe("pow", function() {
});
The principal difference is that when assert triggers an error, the it block immediately
terminates. So, in the first variant if the first assert fails, then we’ll never see the result of the
second assert .
Making tests separate is useful to get more information about what’s going on, so the second
variant is better.
And besides that, there’s one more rule that’s good to follow.
The result:
pow
✓ 2 raised to power 3 is 8 ‣
✖ 3 raised to power 3 is 27 ‣
As we could expect, the second test failed. Sure, our function always returns 8 , while the
assert expects 27 .
function pow(x, n) {
let result = 1;
return result;
}
To be sure that the function works well, let’s test it for more values. Instead of writing it blocks
manually, we can generate them in for :
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);
});
}
});
The result:
pow
✓ 1 in the power 3 is 1 ‣
✓ 2 in the power 3 is 8 ‣
✓ 3 in the power 3 is 27 ‣
✓ 4 in the power 3 is 64 ‣
✓ 5 in the power 3 is 125 ‣
Nested describe
We’re going to add even more tests. But before that let’s note that the helper function
makeTest and for should be grouped together. We won’t need makeTest in other tests, it’s
needed only in for : their common task is to check how pow raises into the given power.
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);
});
}
// ... more tests to follow here, both describe and it can be added
});
The nested describe defines a new “subgroup” of tests. In the output we can see the titled
indentation:
pow
raises x to power 3
✓ 1 in the power 3 is 1 ‣
✓ 2 in the power 3 is 8 ‣
✓ 3 in the power 3 is 27 ‣
✓ 4 in the power 3 is 64 ‣
✓ 5 in the power 3 is 125 ‣
In the future we can add more it and describe on the top level with helper functions of their
own, they won’t see makeTest .
before/after and beforeEach/afterEach
We can setup before/after functions that execute before/after running tests, and also
beforeEach/afterEach functions that execute before/after every it .
For instance:
describe("test", function() {
});
The basic functionality of pow is complete. The first iteration of the development is done. When
we’re done celebrating and drinking champagne – let’s go on and improve it.
As it was said, the function pow(x, n) is meant to work with positive integer values n .
To indicate a mathematical error, JavaScript functions usually return NaN . Let’s do the same for
invalid values of n .
describe("pow", function() {
// ...
});
pow
✖ if n is negative, the result is NaN ‣
raises x to power 3
✓ 1 in the power 3 is 1 ‣
✓ 2 in the power 3 is 8 ‣
✓ 3 in the power 3 is 27 ‣
✓ 4 in the power 3 is 64 ‣
✓ 5 in the power 3 is 125 ‣
The newly added tests fail, because our implementation does not support them. That’s how BDD
is done: first we write failing tests, and then make an implementation for them.
Other assertions
Please note the assertion assert.isNaN : it checks for NaN .
function pow(x, n) {
if (n < 0) return NaN;
if (Math.round(n) != n) return NaN;
let result = 1;
return result;
}
pow
✓ if n is negative, the result is NaN ‣
✓ if n is not integer, the result is NaN ‣
raises x to power 3
✓ 1 in the power 3 is 1 ‣
✓ 2 in the power 3 is 8 ‣
✓ 3 in the power 3 is 27 ‣
✓ 4 in the power 3 is 64 ‣
✓ 5 in the power 3 is 125 ‣
Summary
In BDD, the spec goes first, followed by implementation. At the end we have both the spec and
the code.
The spec can be used in three ways:
With the spec, we can safely improve, change, even rewrite the function from scratch and make
sure it still works right.
That’s especially important in large projects when a function is used in many places. When we
change such a function, there’s just no way to manually check if every place that uses it still
works right.
Naturally, that’s because auto-tested code is easier to modify and improve. But there’s also
another reason.
To write tests, the code should be organized in such a way that every function has a clearly
described task, well-defined input and output. That means a good architecture from the
beginning.
In real life that’s sometimes not that easy. Sometimes it’s difficult to write a spec before the actual
code, because it’s not yet clear how it should behave. But in general writing tests makes
development faster and more stable.
Later in the tutorial you will meet many tasks with tests baked-in. So you’ll see more practical
examples.
Writing tests requires good JavaScript knowledge. But we’re just starting to learn it. So, to settle
down everything, as of now you’re not required to write tests, but you should already be able to
read them even if they are a little bit more complex than in this chapter.
✔ Tasks
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);
});
To solution
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
https://tc39.github.io/ecma262/ and then progress to the specification .
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
https://kangax.github.io/compat-table/es6/ (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 is a transpiler . It rewrites modern JavaScript code into the previous standard.
1. First, the transpiler program, which rewrites the code. The developer runs it on their own
computer. It rewrites the code into the older standard. And then the code is delivered to the
website for users. Modern project build systems like webpack provide means to run
transpiler automatically on every code change, so that it’s very easy to integrate into
development process.
So, if we’re going to use modern language features, a transpiler and a polyfill are necessary.
As you’re reading the offline version, in PDF examples are not runnable. In EPUB some of them
can run.
Google Chrome is usually the most up-to-date with language features, good to run bleeding-edge
demos without any transpilers, but other modern browsers also work fine.
We can imagine an object as a cabinet with signed files. Every piece of data is stored in its file by
the key. It’s easy to find a file by its name or add/remove a file.
key1
key2
key3
An empty object (“empty cabinet”) can be created using one of two syntaxes:
empty
user
Usually, the figure brackets {...} are used. That declaration is called an object literal.
We can immediately put some properties into {...} as “key: value” pairs:
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 labeled “name”
and “age”.
name
age
user
user.isAdmin = true;
isAdmin
name
age
user
delete user.age;
isAdmin
name
user
We can also use multiword property names, but then they must be quoted:
let user = {
name: "John",
age: 30,
"likes birds": true // multiword property name must be quoted
};
name
likes birds
age
user
let user = {
name: "John",
age: 30,
}
Square brackets
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:
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 literal string – like from a variable as follows:
Here, the variable key may be calculated at run-time or depend on the user input. And then we
use it to access the property. That gives us a great deal of flexibility.
For instance:
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")
let user = {
name: "John",
age: 30
};
Computed properties
We can use square brackets in an object literal. That’s called computed properties.
For instance:
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {
[fruit]: 5, // the name of the property is taken from the variable fruit
};
The meaning of a computed property is simple: [fruit] means that the property name should
be taken from fruit .
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.
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:
let obj = {
for: 1,
let: 2,
return: 3
};
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:
That can become a source of bugs and even vulnerabilities if we intend to store arbitrary key-
value pairs in an object, and allow a visitor to specify the keys.
In that case the visitor may choose __proto__ as the key, and the assignment logic will be
ruined (as shown above).
There is a way to make objects treat __proto__ as a regular property, which we’ll cover
later, but first we need to know more about objects.
There’s also another data structure Map, that we’ll learn in the chapter Map and Set, which
supports arbitrary keys.
In real code we often use existing variables as values for property names.
For instance:
We can use both normal properties and shorthands in the same object:
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:
There also exists a special operator "in" to check for the existence of a property.
"key" in object
For instance:
Please note that on the left side of in there must be a property name. That’s usually a quoted
string.
If we omit quotes, that would mean a variable containing the actual name will be tested. For
instance:
let obj = {
test: undefined
};
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.
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:
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 us to declare the looping variable inside the loop, like let
key here.
Also, we could use another variable name here instead of key . For instance, "for (let
prop in obj)" is also widely used.
The short answer is: “ordered in a special fashion”: integer properties are sorted, others appear in
creation order. The details follow.
let codes = {
"49": "Germany",
"41": "Switzerland",
"44": "Great Britain",
// ..,
"1": "USA"
};
The object may be used to suggest a list of options to the user. If we’re making a site mainly for
German audience then we probably want 49 to be the first.
The phone codes go in the ascending sorted order, because they are integers. So we see 1,
41, 44, 49 .
Integer properties? What’s that?
The “integer property” term here means a string that can be converted to-and-from an integer
without a change.
So, “49” is an integer property name, because when it’s transformed to an integer number
and back, it’s still the same. But “+49” and “1.2” are not:
…On the other hand, if the keys are non-integer, then they are listed in the creation order, for
instance:
let user = {
name: "John",
surname: "Smith"
};
user.age = 25; // add one more
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:
let codes = {
"+49": "Germany",
"+41": "Switzerland",
"+44": "Great Britain",
// ..,
"+1": "USA"
};
Copying by reference
One of the fundamental differences of objects vs primitives is that they are stored and copied “by
reference”.
Primitive values: strings, numbers, booleans – are assigned/copied “as a whole value”.
For instance:
As a result we have two independent variables, each one is storing the string "Hello!" .
"H
"H
ell
ell
o
o
!"
!"
message phrase
A variable stores not the object itself, but its “address in memory”, in other words “a
reference” to it.
let user = {
name: "John"
};
name
user
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:
name
user admin
We can use any variable to access the cabinet and modify its contents:
The example above demonstrates that there is only one object. As if we had a cabinet with two
keys and used one of them ( admin ) to get into it. Then, if we later use the other key ( user ) we
would see changes.
Comparison by reference
The equality == and strict equality === operators for objects work exactly the same.
Two objects are equal only if they are the same object.
For instance, if two variables reference the same object, they are equal:
let a = {};
let b = a; // copy the reference
And here two independent objects are not equal, even though both are empty:
let a = {};
let b = {}; // two independent objects
alert( a == b ); // false
For comparisons like obj1 > obj2 or for a comparison against a primitive obj == 5 ,
objects are converted to primitives. We’ll study how object conversions work very soon, but to tell
the truth, such comparisons are necessary very rarely and usually are a result of a coding
mistake.
Const object
An object declared as const can be changed.
For instance:
const user = {
name: "John"
};
alert(user.age); // 25
It might seem that the line (*) would cause an error, but no, there’s totally no problem. That’s
because const fixes only value of user itself. And here user stores the reference to the
same object all the time. The line (*) goes inside the object, it doesn’t reassign user .
The const would give an error if we try to set user to something else, for instance:
const user = {
name: "John"
};
…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 Property flags and descriptors.
So, copying an object variable creates one more reference to the same object.
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:
let user = {
name: "John",
age: 30
};
let clone = {}; // the new empty object
●
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 .
If the receiving object ( user ) already has the same named property, it will be overwritten:
We also can use Object.assign to replace the loop for simple cloning:
let user = {
name: "John",
age: 30
};
let clone = Object.assign({}, user);
It copies all properties of user into the empty object and returns it. Actually, the same as the
loop, but shorter.
Until now we assumed that all properties of user are primitive. But properties can be references
to other objects. What to do with them?
Like this:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
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:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
To fix that, we should use the cloning loop that examines each value of user[key] and, if it’s
an object, then replicate its structure as well. That is called a “deep cloning”.
There’s a standard algorithm for deep cloning that handles the case above and more complex
cases, called the Structured cloning algorithm . In order not to reinvent the wheel, we can use
a working implementation of it from the JavaScript library lodash , the method is called
_.cloneDeep(obj) .
Summary
Additional operators:
●
To delete a property: delete obj.prop .
● To check if a property with the given key exists: "key" in obj .
● To iterate over an object: for (let key in obj) loop.
Objects are assigned and copied by reference. In other words, a variable stores not the “object
value”, but a “reference” (address in memory) for the value. So copying such a variable or
passing it as a function argument copies that reference, not the object. All operations via copied
references (like adding/removing properties) are performed on the same single object.
What we’ve studied in this chapter is called a “plain object”, or just Object .
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 a topic that is
really huge. We’ll be closely working with objects and learning more about them in further parts of
the tutorial.
✔ Tasks
Hello, object
importance: 5
To solution
Write the function isEmpty(obj) which returns true if the object has no properties, false
otherwise.
To solution
Constant objects?
importance: 5
const user = {
name: "John"
};
// does it work?
user.name = "Pete";
To solution
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.
To solution
For instance:
multiplyNumeric(menu);
Please note that multiplyNumeric does not need to return anything. It should modify the
object in-place.
To solution
Garbage collection
Memory management in JavaScript is performed automatically and invisibly to us. We create
primitives, objects, functions… All that takes memory.
What happens when something is not needed any more? How does the JavaScript engine
discover it and clean it up?
Reachability
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)
2. Any other value is considered reachable if it’s reachable from a root by a reference or by a
chain of references.
For instance, if there’s an object in a local variable, and that object has a property referencing
another object, that object is considered reachable. And those that it references are also
reachable. Detailed examples to follow.
There’s a background process in the JavaScript engine that is called garbage collector . It
monitors all objects and removes those that have become unreachable.
A simple example
<global>
user
Object
name: "John"
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:
user = null;
<global>
user: null
Object
name: "John"
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
<global>
user admin
Object
name: "John"
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
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.
<global variable>
family
Object
father mother
wife
Object Object
name: "John" husband name: "Ann"
delete family.father;
delete family.mother.husband;
<global variable>
family
Object
father mother
wife
Object Object
name: "John" husband name: "Ann"
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:
<global>
family
Object
mother
wife
Object Object
name: "John" name: "Ann"
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:
<global>
family
Object
mother
Object
name: "Ann"
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:
family = null;
<global>
family: null
Object
father mother
wife
Object Object
name: "John" husband name: "Ann"
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
<global>
We can clearly see an “unreachable island” to the right side. Now let’s see how “mark-and-
sweep” garbage collector deals with it.
<global>
<global>
Now the objects that could not be visited in the process are considered unreachable and will be
removed:
<global> unreachables
We can also imagine the process as spilling a huge bucket of paint from the roots, that flows
through all references and marks all reachable objects. The unmarked ones are then removed.
That’s the concept of how garbage collection works. JavaScript engines apply many
optimizations to make it run faster and not affect the execution.
Summary
A general book “The Garbage Collection Handbook: The Art of Automatic Memory Management”
(R. Jones et al) covers some of them.
If you are familiar with low-level programming, the more detailed information about V8 garbage
collector is in the article A tour of V8: Garbage Collection .
V8 blog 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 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.
Symbol type
By specification, object property keys may be either of string type, or of symbol type. Not
numbers, not booleans, only strings or symbols, these two types.
Till now we’ve been using only strings. Now let’s see the benefits that symbols can give us.
Symbols
// id is a new symbol
let id = Symbol();
Upon creation, we can give symbol a description (also called a symbol name), mostly useful for
debugging purposes:
// id is a symbol with the description "id"
let id = Symbol("id");
Symbols are guaranteed to be unique. Even if we create many symbols with the same
description, they are different values. The description is just a label that doesn’t affect anything.
For instance, here are two symbols with the same description – they are not equal:
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.
let id = Symbol("id");
alert(id); // TypeError: Cannot convert a Symbol value to a string
That’s a “language guard” against messing up, because strings and symbols are
fundamentally different and should not accidentally convert one into another.
If we really want to show a symbol, we need to explicitly call .toString() on it, like here:
let id = Symbol("id");
alert(id.toString()); // Symbol(id), now it works
let id = Symbol("id");
alert(id.description); // id
“Hidden” properties
Symbols allow us to create “hidden” properties of an object, that no other part of code can
accidentally access or overwrite.
For instance, if we’re working with user objects, that belong to a third-party code. We’d like to
add identifiers to them.
Let’s use a symbol key for it:
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // we can access the data using the symbol as the key
As user objects belongs to another code, and that code also works with them, we shouldn’t just
add any fields to it. That’s unsafe. But a symbol cannot be accessed accidentally, the third-party
code probably won’t even see it, so it’s probably all right to do.
Also, imagine that another script wants to have its own identifier inside user , for its own
purposes. That may be another JavaScript library, so that the scripts are completely unaware of
each other.
Then that script can create its own Symbol("id") , like this:
// ...
let id = Symbol("id");
There will be no conflict between our and their identifiers, because symbols are always different,
even if they have the same name.
…But if we used a string "id" instead of a symbol for the same purpose, then there would be a
conflict:
Symbols in a literal
If we want to use a symbol in an object literal {...} , we need square brackets around it.
Like this:
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // not "id: 123"
};
That’s because we need the value from the variable id as the key, not the string “id”.
For instance:
let id = Symbol("id");
let user = {
name: "John",
age: 30,
[id]: 123
};
Object.keys(user) also ignores them. That’s a part of the general “hiding symbolic
properties” principle. If another script or a library loops over our object, it won’t unexpectedly
access a symbolic property.
let id = Symbol("id");
let user = {
[id]: 123
};
There’s no paradox here. That’s by design. The idea is that when we clone an object or merge
objects, we usually want all properties to be copied (including symbols like id ).
Property keys of other types are coerced to strings
We can only use strings or symbols as keys in objects. Other types are converted to strings.
For instance, a number 0 becomes a string "0" when used as a property key:
let obj = {
0: "test" // same as "0": "test"
};
// both alerts access the same property (the number 0 is converted to string "0")
alert( obj["0"] ); // test
alert( obj[0] ); // test (same property)
Global symbols
As we’ve seen, usually all symbols are different, even if they have the same name. But
sometimes we want same-named symbols to be same entities. For instance, different parts of our
application want to access symbol "id" meaning exactly the same property.
To achieve that, there exists a global symbol registry. We can create symbols in it and access
them later, and it guarantees that repeated accesses by the same name return exactly the same
symbol.
In order to read (create if absent) a symbol from the registry, use Symbol.for(key) .
That call checks the global registry, and if there’s a symbol described as key , then returns it,
otherwise creates a new symbol Symbol(key) and stores it in the registry by the given key .
For instance:
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.
Symbol.keyFor
For global symbols, not only Symbol.for(key) returns a symbol by name, but there’s a
reverse call: Symbol.keyFor(sym) , that does the reverse: returns a name by a global
symbol.
For instance:
The Symbol.keyFor internally uses the global symbol registry to look up the key for the
symbol. So it doesn’t work for non-global symbols. If the symbol is not global, it won’t be able to
find it and return undefined .
For instance:
System symbols
There exist many “system” symbols that JavaScript uses internally, and we can use them to fine-
tune various aspects of our objects.
Summary
Symbols are created with Symbol() call with an optional description (name).
Symbols are always different values, even if they have the same name. If we want same-named
symbols to be equal, then we should use the global registry: Symbol.for(key) returns
(creates if needed) a global symbol with key as the name. Multiple calls of Symbol.for with
the same key return exactly the same symbol.
1. “Hidden” object properties. If we want to add a property into an object that “belongs” to another
script or a library, we can create a symbol and use it as a property key. A symbolic property
does not appear in for..in , so it won’t be accidentally processed together with other
properties. Also it won’t be accessed directly, because another script does not have our
symbol. So the property will be protected from accidental use or overwrite.
So we can “covertly” hide something into objects that we need, but others should not see,
using symbolic properties.
2. There are many system symbols used by JavaScript which are accessible as Symbol.* . We
can use them to alter some built-in behaviors. For instance, later in the tutorial we’ll use
Symbol.iterator for iterables, Symbol.toPrimitive to setup object-to-primitive
conversion and so on.
let user = {
name: "John",
age: 30
};
And, in the real world, a user can act: select something from the shopping cart, login, logout etc.
Method examples
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.
let user = {
// ...
};
// first, declare
function sayHi() {
alert("Hello!");
};
user.sayHi(); // Hello!
Object-oriented programming
When we write our code using objects to represent entities, that’s called 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.
Method shorthand
There exists a shorter syntax for methods in an object literal:
user = {
sayHi: function() {
alert("Hello");
}
};
To tell the truth, the notations are not fully identical. There are subtle differences related to object
inheritance (to be covered later), but for now they do not matter. In almost all cases the shorter
syntax is preferred.
“this” in methods
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 .
The value of this is the object “before dot”, the one used to call the method.
For instance:
let user = {
name: "John",
age: 30,
sayHi() {
// "this" is the "current object"
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:
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.
sayHi() {
alert( user.name ); // leads to an error
}
};
If we used this.name instead of user.name inside the alert , then the code would work.
In JavaScript, keyword this behaves unlike most other programming languages. It can be used
in any function.
There’s no syntax error in the following example:
function sayHi() {
alert( this.name );
}
The value of this is evaluated during the run-time, depending on the context.
For instance, here the same function is assigned to two different objects and has different “this” in
the calls:
function sayHi() {
alert( this.name );
}
admin['f'](); // Admin (dot or square brackets access the method – doesn't matter)
The rule is simple: if obj.f() is called, then this is obj during the call of f . So it’s either
user or admin in the example above.
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 the value of this in such case will be the global object ( window in a
browser, we’ll get to it later in the chapter Global object). This is a historical behavior that
"use strict" fixes.
Usually such call is a programming error. If there’s this inside a function, it expects to be
called in an object context.
In JavaScript this is “free”, its value is evaluated at call-time and does not depend on
where the method was declared, but rather on what object is “before the dot”.
The concept of run-time evaluated this has both pluses and minuses. On the one hand, a
function can be reused for different objects. On the other hand, the greater flexibility creates
more possibilities for mistakes.
Here our position is not to judge whether this language design decision is good or bad. We’ll
understand how to work with it, how to get benefits and avoid problems.
let user = {
name: "John",
hi() { alert(this.name); },
bye() { alert("Bye"); }
};
On the last line there is a conditional operator that chooses either user.hi or user.bye . In
this case the result is user.hi .
Then the method is immediately called with parentheses () . But it doesn’t work correctly!
As you can see, the call results in an error, because the value of "this" inside the call
becomes undefined .
user.hi();
Why? If we want to understand why it happens, let’s get under the hood of how obj.method()
call works.
So, how does the information about this get passed from the first part to the second one?
If we put these operations on separate lines, then this will be lost for sure:
let user = {
name: "John",
hi() { alert(this.name); }
}
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 .
The Reference Type is a “specification type”. We can’t explicitly use it, but it is used internally by
the language.
The value of Reference Type is a three-value combination (base, name, strict) , where:
● base is the object.
● name is the property name.
●
strict is true if use strict is in effect.
The result of a property access user.hi is not a function, but a value of Reference Type. For
user.hi in strict mode it is:
When parentheses () are called on the Reference Type, they receive the full information about
the object and its method, and can set the right this ( =user in this case).
Reference type is a special “intermediary” internal type, with the purpose to pass information from
dot . to calling parentheses () .
Any other operation like assignment hi = user.hi discards the reference type as a whole,
takes the value of user.hi (a function) and passes it on. So any further operation “loses”
this .
So, as the result, the value of this is only passed the right way if the function is called directly
using a dot obj.method() or square brackets obj['method']() syntax (they do the same
here). Later in this tutorial, we will learn various ways to solve this problem such as func.bind().
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:
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 Arrow functions
revisited 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 .
Please note that arrow functions are special: they have no this . When this is accessed
inside an arrow function, it is taken from outside.
✔ Tasks
Syntax check
importance: 2
let user = {
name: "John",
go: function() { alert(this.name) }
}
(user.go)()
To solution
But calls (1) and (2) works differently from (3) and (4) . Why?
obj = {
go: function() { alert(this); }
};
To solution
function makeUser() {
return {
name: "John",
ref: this
};
};
To solution
Create a calculator
importance: 5
● 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.
let calculator = {
// ... your code ...
};
calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );
To solution
Chaining
importance: 2
let ladder = {
step: 0,
up() {
this.step++;
},
down() {
this.step--;
},
showStep: function() { // shows the current step
alert( this.step );
}
};
ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1
Modify the code of up , down and showStep to make the calls chainable, like this:
ladder.up().up().down().showStep(); // 1
To solution
In that case, objects are auto-converted to primitives, and then the operation is carried out.
In the chapter Type Conversions we’ve seen the rules for numeric, string and boolean
conversions of primitives. But we left a gap for objects. Now, as we know about methods and
symbols it becomes possible to fill it.
1. All objects are true in a boolean context. There are only numeric and string conversions.
2. The numeric conversion happens when we subtract objects or apply mathematical functions.
For instance, Date objects (to be covered in the chapter Date and time) can be subtracted,
and the result of date1 - date2 is the time difference between two dates.
3. As for the string conversion – it usually happens when we output an object like alert(obj)
and in similar contexts.
ToPrimitive
We can fine-tune string and numeric conversion, using special object methods.
There are three variants of type conversion, so-called “hints”, described in the specification :
"string"
For an object-to-string conversion, when we’re doing an operation on an object that expects a
string, like alert :
// output
alert(obj);
"number"
For an object-to-number conversion, like when we’re doing maths:
// explicit conversion
let num = Number(obj);
// 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. So if the a binary plus gets an object as an
argument, it uses the "default" hint to convert it.
Also, if an object is compared using == with a string, number or a symbol, it’s also unclear which
conversion should be done, so the "default" hint is used.
The greater and less comparison operators, such as < > , can work with both strings and
numbers too. Still, they use the "number" hint, not "default" . That’s for historical reasons.
In practice though, we don’t need to remember these peculiar details, because all built-in objects
except for one case ( Date object, we’ll learn it later) implement "default" conversion the
same way as "number" . And we can do the same.
No "boolean" hint
Please note – there are only three hints. It’s that simple.
There is no “boolean” hint (all objects are true in boolean context) or anything else. And if
we treat "default" and "number" the same, like most built-ins do, then there are only
two conversions.
To do the conversion, JavaScript tries to find and call three object methods:
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:
obj[Symbol.toPrimitive] = function(hint) {
// must return a primitive value
// hint = one of "string", "number", "default"
};
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.
These methods must return a primitive value. If toString or valueOf returns an object, then
it’s ignored (same as if there were no method).
So if we try to use an object as a string, like in an alert or so, then by default we see
[object Object] .
And the default valueOf is mentioned here only for the sake of completeness, to avoid any
confusion. As you can see, it returns the object itself, and so is ignored. Don’t ask me why, that’s
for historical reasons. So we can assume it doesn’t exist.
For instance, here user does the same as above using a combination of toString and
valueOf instead of Symbol.toPrimitive :
let user = {
name: "John",
money: 1000,
// for hint="string"
toString() {
return `{name: "${this.name}"}`;
},
};
As we can see, the behavior is the same as the previous example with Symbol.toPrimitive .
Often we want a single “catch-all” place to handle all primitive conversions. In this case, we can
implement toString only, like this:
let user = {
name: "John",
toString() {
return this.name;
}
};
In the absence of Symbol.toPrimitive and valueOf , toString will handle all primitive
conversions.
Return types
The important thing to know about all primitive-conversion methods is that they do not necessarily
return the “hinted” primitive.
There is no control whether toString returns exactly a string, or whether
Symbol.toPrimitive method returns a number for a hint "number" .
The only mandatory thing: these methods must return a primitive, not an object.
Historical notes
For historical reasons, if toString or valueOf returns an object, there’s no error, but
such value is ignored (like if the method didn’t exist). That’s because in ancient times there
was no good “error” concept in JavaScript.
In contrast, Symbol.toPrimitive must return a primitive, otherwise there will be an error.
Further conversions
As we know already, many operators and functions perform type conversions, e.g. multiplication
* converts operands to numbers.
For instance:
let obj = {
// toString handles all conversions in the absence of other methods
toString() {
return "2";
}
};
alert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number
1. The multiplication obj * 2 first converts the object to primitive (that’s a string "2" ).
2. Then "2" * 2 becomes 2 * 2 (the string is converted to number).
Binary plus will concatenate strings in the same situation, as it gladly accepts a string:
let obj = {
toString() {
return "2";
}
};
alert(obj + 2); // 22 ("2" + 2), conversion to primitive returned a string => concatenation
Summary
The object-to-primitive conversion is called automatically by many built-in functions and operators
that expect a primitive as a value.
There are 3 types (hints) of it:
●
"string" (for alert and other operations that need a string)
●
"number" (for maths)
●
"default" (few operators)
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.
In practice, it’s often enough to implement only obj.toString() as a “catch-all” method for all
conversions that return a “human-readable” representation of an object, for logging or debugging
purposes.
Constructor, operator "new"
The regular {...} syntax allows to create one object. But often we need to create many similar
objects, like multiple users or menu items and so on.
That can be done using constructor functions and the "new" operator.
Constructor function
Constructor functions technically are regular functions. There are two conventions though:
For instance:
function User(name) {
this.name = name;
this.isAdmin = false;
}
alert(user.name); // Jack
alert(user.isAdmin); // false
function User(name) {
// this = {}; (implicitly)
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 easy to read.
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 .
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:
The constructor can’t be called again, because it is not saved anywhere, just created and
called. So this trick aims to encapsulate the code that constructs the single object, without
future reuse.
Advanced stuff
The syntax from this section is rarely used, skip it unless you want to know everything.
Inside a function, we can check whether it was called with new or without it, using a special
new.target property.
It is empty for regular calls and equals the function if called with new :
function User() {
alert(new.target);
}
// without "new":
User(); // undefined
// with "new":
new User(); // function User { ... }
That can be used inside the function to know whether it was called with new , “in constructor
mode”, or without it, “in regular mode”.
We can also make both new and regular calls to do the same, like this:
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;
}
This approach is sometimes used in libraries to make the syntax more flexible. So that people
may call the function with or without new , and it still works.
Probably not a good thing to use everywhere though, because omitting new makes it a bit less
obvious what’s going on. With new we all know that the new object is being created.
Usually, constructors do not have a return statement. Their task is to write all necessary stuff
into this , and it automatically becomes the result.
In other words, return with an object returns that object, in all other cases this is returned.
function BigUser() {
this.name = "John";
And here’s an example with an empty return (or we could place a primitive after it, doesn’t
matter):
function SmallUser() {
this.name = "John";
Omitting parentheses
By the way, we can omit parentheses after new , if it has no arguments:
Omitting parentheses here is not considered a “good style”, but the syntax is permitted by
specification.
Methods in constructor
Using constructor functions to create objects gives a great deal of flexibility. The constructor
function may have parameters that define how to construct the object, and 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 :
function User(name) {
this.name = name;
this.sayHi = function() {
alert( "My name is: " + this.name );
};
}
/*
john = {
name: "John",
sayHi: function() { ... }
}
*/
To create complex objects, there’s a more advanced syntax, classes, that we’ll cover later.
Summary
●
Constructor functions or, briefly, constructors, are regular functions, but there’s a common
agreement to name them with capital letter first.
● Constructor functions should only be called using new . Such a 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.
After we learn that, we return to objects and cover them in-depth in the chapters Prototypes,
inheritance and Classes.
✔ Tasks
let a = new A;
let b = new B;
alert( a == b ); // true
To solution
● 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:
To solution
● 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 .
To solution
Data types
More data structures and more in-depth study of the types.
Methods of primitives
JavaScript allows us to work with primitives (strings, numbers, etc.) as if they were objects. They
also provide methods to call as such. We will study those soon, but first we’ll see how it works
because, of course, primitives are not objects (and here we will make it even clearer).
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: functions, for example, are objects.
One of the best things about objects is that we can store a function as one of its properties.
let john = {
name: "John",
sayHi: function() {
alert("Hi buddy!");
}
};
john.sayHi(); // Hi buddy!
Many built-in objects already exist, such as those that work with dates, errors, HTML elements,
etc. They have different properties and methods.
Objects are “heavier” than primitives. They require additional resources to support the internal
machinery.
A primitive as an object
The “object wrappers” are different for each primitive type and are called: String , Number ,
Boolean and Symbol . Thus, they provide different sets of methods.
For instance, there exists a string method str.toUpperCase() that returns a capitalized str .
1. The string str is a primitive. So in the moment of accessing its property, a special object is
created that 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.
The JavaScript engine highly optimizes this process. It may even skip the creation of the extra
object at all. But it must still adhere to the specification and behave as if it creates one.
A number has methods of its own, for instance, toFixed(n) rounds the number to the given
precision:
let n = 1.23456;
In JavaScript, that’s also possible for historical reasons, but highly unrecommended. Things
will go crazy in several places.
For instance:
Objects are always truthy in if , so here the alert will show up:
On the other hand, using the same functions String/Number/Boolean without new is a
totally sane and useful thing. They convert a value to the corresponding type: to a string, a
number, or a boolean (primitive).
alert(null.test); // error
Summary
● Primitives except null and undefined provide many helpful methods. We will study those
in the upcoming chapters.
●
Formally, these methods work via temporary objects, but JavaScript engines are well tuned to
optimize that internally, so they are not expensive to call.
✔ Tasks
str.test = 5;
alert(str.test);
To solution
Numbers
In modern JavaScript, there are two types of numbers:
1. Regular numbers in JavaScript are stored in 64-bit format IEEE-754 , also known as “double
precision floating point numbers”. These are numbers that we’re using most of the time, and
we’ll talk about them in this chapter.
2. BigInt numbers, to represent integers of arbitrary length. They are sometimes needed,
because a regular number can’t exceed 253 or be less than -253 . As bigints are used in few
special areas, we devote them a special chapter BigInt.
So here we’ll talk about regular numbers. Let’s expand our knowledge of them.
But in real life, we usually avoid writing a long string of zeroes as it’s easy to mistype. Also, we
are lazy. We will usually write something like "1bn" for a billion or "7.3bn" for 7 billion 300
million. The same is true for most large numbers.
In JavaScript, we shorten a number by appending the letter "e" to the number and specifying
the zeroes count:
1e3 = 1 * 1000
1.23e6 = 1.23 * 1000000
Now let’s write something very small. Say, 1 microsecond (one millionth of a second):
let ms = 0.000001;
Just like before, using "e" can help. If we’d like to avoid writing the zeroes explicitly, we could
say the same as:
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:
Binary and octal numeral systems are rarely used, but also supported using the 0b and 0o
prefixes:
There are only 3 numeral systems with such support. For other numeral systems, we should use
the function parseInt (which we will see later in this chapter).
toString(base)
For example:
alert( num.toString(16) ); // ff
alert( num.toString(2) ); // 11111111
Rounding
One of the most used operations when working with numbers is 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 .
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 of the possible ways to deal with the decimal part of a number. But what
if we’d like to round the number to n-th digit after the decimal?
For instance, we have 1.2345 and want to round it to 2 digits, getting only 1.23 .
1. Multiply-and-divide.
For example, to round the number to the 2nd digit after the decimal, we can multiply the
number by 100 , call the rounding function and then divide it back.
alert( Math.floor(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
2. The method toFixed(n) rounds the number to n digits after the point and returns a string
representation of the result.
Please note that result of toFixed is a string. If the decimal part is shorter than required,
zeroes are appended to the end:
let num = 12.34;
alert( num.toFixed(5) ); // "12.34000", added zeroes to make exactly 5 digits
Imprecise calculations
Internally, a number is represented in 64-bit format IEEE-754 , so there are exactly 64 bits to
store a number: 52 of them are used to store the digits, 11 of them store the position of the
decimal point (they are zero for integer numbers), and 1 bit is for the sign.
If a number is too big, it would overflow the 64-bit storage, potentially giving an infinity:
What may be a little less obvious, but happens quite often, is the loss of precision.
That’s right, if we check whether the sum of 0.1 and 0.2 is 0.3 , we get false .
Ouch! There are more consequences than an incorrect comparison here. Imagine you’re making
an e-shopping site and the visitor puts $0.10 and $0.20 goods into their cart. The order total
will be $0.30000000000000004 . That would surprise anyone.
A number is stored in memory in its binary form, a sequence of bits – ones and zeroes. But
fractions like 0.1 , 0.2 that look simple in the decimal numeric system are actually unending
fractions in their binary form.
In other words, what is 0.1 ? It is one divided by ten 1/10 , one-tenth. In decimal numeral
system such numbers are easily representable. Compare it to one-third: 1/3 . It becomes an
endless fraction 0.33333(3) .
So, division by powers 10 is guaranteed to work well in the decimal system, but division by 3 is
not. For the same reason, in the binary numeral system, the division by powers of 2 is
guaranteed to work, but 1/10 becomes an endless binary fraction.
There’s just no way to store exactly 0.1 or exactly 0.2 using the binary system, just like there is no
way to store one-third as a decimal fraction.
The numeric format IEEE-754 solves this by rounding to the nearest possible number. These
rounding rules normally don’t allow us to see that “tiny precision loss”, but it exists.
And when we sum two numbers, their “precision losses” add up.
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, the most reliable method is to round the result with the
help of a method toFixed(n) :
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:
We also can temporarily multiply the numbers by 100 (or a bigger number) to turn them into
integers, do the maths, and then divide back. Then, as we’re doing maths with integers, the error
somewhat decreases, but we still get it on division:
So, multiply/divide approach reduces the error, but doesn’t remove it totally.
Sometimes we could try to evade fractions at all. Like if we’re dealing with a shop, then we can
store prices in cents instead of dollars. But what if we apply a discount of 30%? In practice, totally
evading fractions is rarely possible. Just round them to cut “tails” when needed.
The funny thing
Try running this:
This suffers from the same issue: a loss of precision. There are 64 bits for the number, 52 of
them can be used to store digits, but that’s not enough. So the least significant digits
disappear.
JavaScript doesn’t trigger an error in such events. It does its best to fit the number into the
desired format, but unfortunately, this format is not big enough.
Two zeroes
Another funny consequence of the internal representation of numbers is the existence of two
zeroes: 0 and -0 .
That’s because a sign is represented by a single bit, so it can be set or not set for any number
including a zero.
In most cases the distinction is unnoticeable, because operators are suited to treat them as
the same.
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 it for being NaN :
But do we need this function? Can’t we just use the comparison === NaN ? Sorry, but the
answer is no. The value NaN is unique in that it does not equal anything, including itself:
●
isFinite(value) converts its argument to a number and returns true if it’s a regular
number, not NaN/Infinity/-Infinity :
alert( isFinite("15") ); // true
alert( isFinite("str") ); // false, because a special value: NaN
alert( isFinite(Infinity) ); // false, because a special value: Infinity
Please note that an empty or a space-only string is treated as 0 in all numeric functions
including isFinite .
1. It works with NaN : Object.is(NaN, NaN) === true , that’s a good thing.
2. Values 0 and -0 are different: Object.is(0, -0) === false , technically that’s
true, because internally the number has a sign bit that may be different even if all other bits
are zeroes.
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 ).
Numeric conversion using a plus + or Number() is strict. If a value is not exactly a number, it
fails:
The sole exception is spaces at the beginning or at the end of the string, as they are ignored.
But in real life we often have values in units, like "100px" or "12pt" in CSS. Also in many
countries the currency symbol goes after the amount, so we have "19€" and would like to
extract a numeric value out of that.
They “read” a number from a string until they can’t. In case of an error, the gathered number is
returned. The function parseInt returns an integer, whilst parseFloat will return a floating-
point number:
alert( parseInt('100px') ); // 100
alert( parseFloat('12.5em') ); // 12.5
There are situations when parseInt/parseFloat will return NaN . It happens when no digits
could be read:
JavaScript has a built-in 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)
Math.pow(n, power)
Returns n raised the given power
alert( Math.pow(2, 10) ); // 2 in power 10 = 1024
There are more functions and constants in Math object, including trigonometry, which you can
find in the docs for the Math object.
Summary
For fractions:
●
Round using Math.floor , Math.ceil , Math.trunc , Math.round or
num.toFixed(precision) .
●
Make sure to remember there’s a loss of precision when working with fractions.
✔ Tasks
Create a script that prompts the visitor to enter two numbers and then shows their sum.
To solution
Why 6.35.toFixed(1) == 6.3?
importance: 4
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:
In the similar example below, why is 6.35 rounded to 6.3 , not 6.4 ?
To solution
Create a function readNumber which prompts for a number until the visitor enters a valid
numeric value.
The visitor can also stop the process by entering an empty line or pressing “CANCEL”. In that
case, the function should return null .
To solution
let i = 0;
while (i != 10) {
i += 0.2;
}
To solution
A random number from min to max
importance: 2
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 ).
To solution
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.
alert( randomInteger(1, 5) ); // 1
alert( randomInteger(1, 5) ); // 3
alert( randomInteger(1, 5) ); // 5
You can use the solution of the previous task as the base.
To solution
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 , it is not tied to the page encoding.
Quotes
Strings can be enclosed within either single quotes, double quotes or backticks:
let single = 'single-quoted';
let double = "double-quoted";
Single and double quotes are essentially the same. Backticks, however, allow us to embed any
expression into the string, by wrapping it in ${…} :
function sum(a, b) {
return a + b;
}
Another advantage of using backticks is that they allow a string to span multiple lines:
Looks natural, right? But single or double quotes do not work this way.
Single and double quotes come from ancient times of language creation when the need for
multiline strings was not taken into account. Backticks appeared much later and thus are more
versatile.
Backticks also allow us to specify a “template function” before the first backtick. The syntax is:
func`string` . The function func is called automatically, receives the string and embedded
expressions and can process them. This is called “tagged templates”. This feature makes it
easier to implement custom templating, but is rarely used in practice. You can read more about it
in the manual .
Special characters
It is still possible to create multiline strings with single and double quotes by using a so-called
“newline character”, written as \n , which denotes a line break:
For example, these two lines are equal, just written differently:
Character Description
\n New line
Carriage return: not used alone. Windows text files use a combination of two characters \r\n to
\r
represent a line break.
\\ Backslash
\t Tab
\b , \f , \v Backspace, Form Feed, Vertical Tab – kept for compatibility, not used nowadays.
\xXX Unicode character with the given hexadecimal unicode XX , e.g. '\x7A' is the same as 'z' .
A unicode symbol with the hex code XXXX in UTF-16 encoding, for instance \u00A9 – is a unicode
\uXXXX
for the copyright symbol © . It must be exactly 4 hex digits.
\u{X…XXXXXX} (1 to A unicode symbol with the given UTF-32 encoding. Some rare characters are encoded with two
6 hex characters) unicode symbols, taking 4 bytes. This way we can insert long codes.
alert( "\u00A9" ); // ©
alert( "\u{20331}" ); // 佫, a rare Chinese hieroglyph (long unicode)
alert( "\u{1F60D}" ); // 😍, a smiling face symbol (another long unicode)
All special characters start with a backslash character \ . It is also called an “escape character”.
For instance:
As you can see, we have to prepend the inner quote by the backslash \' , because otherwise it
would indicate the string end.
Of course, only to the quotes that are the same as the enclosing ones need to be escaped. So,
as a more elegant solution, we could switch to double quotes or backticks instead:
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.
String length
alert( `My\n`.length ); // 3
⚠ length is a property
People with a background in some other languages sometimes mistype by calling
str.length() instead of just str.length . That doesn’t work.
Please note that str.length is a numeric property, not a function. There is no need to add
parenthesis after it.
Accessing characters
To get a character at position pos , use square brackets [pos] or call the method
str.charAt(pos) . The first character starts from the zero position:
The square brackets are a modern way of getting a character, while charAt exists mostly for
historical reasons.
The only difference between them is that if no character is found, [] returns undefined , and
charAt returns an empty string:
The usual workaround is to create a whole new string and assign it to str instead of the old
one.
For instance:
alert( str ); // hi
str.indexOf
The first method is str.indexOf(substr, pos) .
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:
The optional second parameter allows us to search starting from the given position.
For instance, the first occurrence of "id" is at position 1 . To look for the next occurrence, let’s
start the search from position 2 :
alert( str.indexOf('id', 2) ) // 12
If we’re interested in all occurrences, we can run indexOf in a loop. Every new call is made
with the position after the previous match:
let pos = 0;
while (true) {
let foundPos = str.indexOf(target, pos);
if (foundPos == -1) break;
str.lastIndexOf(substr, position)
There is also a similar method str.lastIndexOf(substr, position) that searches from the end
of a string to its beginning.
There is a slight inconvenience with indexOf in the if test. We can’t put it in the if like this:
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 0 to be
false .
if (str.indexOf("Widget") != -1) {
alert("We found it"); // works now!
}
One of the old tricks used here is the bitwise NOT ~ operator. It converts the number to a 32-
bit integer (removes the decimal part if exists) and then reverses all bits in its binary
representation.
In practice, that means a simple thing: for 32-bit integers ~n equals -(n+1) .
For instance:
As we can see, ~n is zero only if n == -1 (that’s for any 32-bit signed integer n ).
So, the test if ( ~str.indexOf("...") ) is truthy only if the result of indexOf is not
-1 . In other words, when there is a match.
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.
To be precise though, as big numbers are truncated to 32 bits by ~ operator, there exist other
numbers that give 0 , the smallest is ~4294967295=0 . That makes such check is correct only if
a string is not that long.
Right now we can see this trick only in the old code, as modern JavaScript provides .includes
method (see below).
It’s the right choice if we need to test for the match, but don’t need its position:
The optional second argument of str.includes is the position to start searching from:
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:
If there is no second argument, then slice goes till the end of the string:
Negative values for start/end are also possible. They mean the position is counted from the
string end:
// 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 .
This is almost the same as slice , but it allows start to be greater than end .
For instance:
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 us to specify the length instead of the
ending position:
slice(start, end) from start to end (not including end ) allows negatives
substr(start, length) from start get length characters allows negative start
Comparing strings
This may lead to strange results if we sort these country names. Usually people would expect
Zealand to come after Österreich in the list.
To understand what happens, let’s review the internal representation of strings in JavaScript.
All strings are encoded using 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 :
String.fromCodePoint(code)
Creates a character by its numeric code
alert( String.fromCodePoint(90) ); // Z
We can also add unicode characters by their codes using \u followed by the hex code:
// 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:
See? Capital characters go first, then a few special ones, then lowercase characters, and Ö near
the end of the output.
The characters are compared by their numeric code. The greater code means that the character
is greater. The code for a (97) is greater than the code for Z (90).
●
All lowercase letters go after uppercase letters because their codes are greater.
● Some letters like Ö stand apart from the main alphabet. Here, it’s code is greater than
anything from a to z .
Correct comparisons
The “right” algorithm to do string comparisons is more complex than it may seem, because
alphabets are different for different languages.
Luckily, all modern browsers (IE10- requires the additional library Intl.JS ) support the
internationalization standard ECMA 402 .
It provides a special method to compare strings in different languages, following their rules.
The call str.localeCompare(str2) returns an integer indicating whether str is less, equal or
greater than str2 according to the language rules:
●
Returns a negative number if str is less than str2 .
●
Returns a positive number if str is greater than str2 .
● Returns 0 if they are equivalent.
For instance:
alert( 'Österreich'.localeCompare('Zealand') ); // -1
This method actually has two additional arguments specified in the documentation , which
allows it to specify the language (by default taken from the environment, letter order depends on
the language) and setup additional rules like case sensitivity or should "a" and "á" be treated
as the same etc.
Internals, Unicode
⚠ Advanced knowledge
The section goes deeper into string internals. This knowledge will be useful for you if you plan
to deal with emoji, rare mathematical or hieroglyphic characters or other rare symbols.
You can skip the section if you don’t plan to support them.
Surrogate pairs
All frequently used characters have 2-byte codes. Letters in most european languages, numbers,
and even most hieroglyphs, have a 2-byte representation.
But 2 bytes only allow 65536 combinations and that’s not enough for every possible symbol. So
rare symbols are encoded with a pair of 2-byte characters called “a surrogate pair”.
Note that surrogate pairs did not exist at the time when JavaScript was created, and thus are not
correctly processed by the language!
We actually have a single symbol in each of the strings above, but the length shows a length
of 2 .
String.fromCodePoint and str.codePointAt are few rare methods that deal with
surrogate pairs right. They recently appeared in the language. Before them, there were only
String.fromCharCode and str.charCodeAt . These methods are actually the same as
fromCodePoint/codePointAt , but don’t work with surrogate pairs.
Getting a symbol can be tricky, because surrogate pairs are treated as two characters:
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.
You will find more ways to deal with surrogate pairs later in the chapter Iterables. There are
probably special libraries for that too, but nothing famous enough to suggest here.
For instance, the letter a can be the base character for: àáâäãåā . Most common “composite”
character have their own code in the UTF-16 table. But not all of them, because there are too
many possible combinations.
To support arbitrary compositions, UTF-16 allows us to use several unicode characters: the base
character followed by one or many “mark” characters that “decorate” it.
For instance, if we have S followed by the special “dot above” character (code \u0307 ), it is
shown as Ṡ.
alert( 'S\u0307' ); // Ṡ
If we need an additional mark above the letter (or below it) – no problem, just add the necessary
mark character.
For instance, if we append a character “dot below” (code \u0323 ), then we’ll have “S with dots
above and below”: Ṩ .
For example:
alert( 'S\u0307\u0323' ); // Ṩ
This provides great flexibility, but also an interesting problem: two characters may visually look
the same, but be represented with different unicode compositions.
For instance:
To solve this, there exists a “unicode normalization” algorithm that brings each string to the single
“normal” form.
It is implemented by str.normalize() .
It’s funny that in our situation normalize() actually brings together a sequence of 3 characters
to one: \u1e68 (S with two dots).
alert( "S\u0307\u0323".normalize().length ); // 1
In reality, this is not always the case. The reason being that the symbol Ṩ is “common enough”,
so UTF-16 creators included it in the main table and gave it the code.
If you want to learn more about normalization rules and variants – they are described in the
appendix of the Unicode standard: Unicode Normalization Forms , but for most practical
purposes the information from this section is enough.
Summary
● There are 3 types of quotes. Backticks allow a string to span multiple lines and embed
expressions ${…} .
●
Strings in JavaScript are encoded using UTF-16.
●
We can use special characters like \n and insert letters by their unicode using \u... .
● 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 more to be found in the manual .
Strings also have methods for doing search/replace with regular expressions. But that’s big topic,
so it’s explained in a separate tutorial section Regular expressions.
✔ Tasks
Write a function ucFirst(str) that returns the string str with the uppercased first character,
for instance:
ucFirst("john") == "John";
To solution
Write a function checkSpam(str) that returns true if str contains ‘viagra’ or ‘XXX’,
otherwise false .
To solution
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:
truncate("What I'd like to tell on this topic is:", 20) = "What I'd like to te…"
To solution
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:
To solution
Arrays
Objects allow you to store keyed collections of values. That’s fine.
But quite often we find that we need an ordered collection, where we have a 1st, a 2nd, a 3rd
element and so on. For example, we need that to store a list of something: users, goods, HTML
elements etc.
It is not convenient to use an object here, because it provides no methods to manage the order of
elements. We can’t insert a new property “between” the existing ones. Objects are just not meant
for such use.
There exists a special data structure named Array , to store ordered collections.
Declaration
Almost all the time, the second syntax is used. We can supply initial elements in the brackets:
let fruits = ["Apple", "Orange", "Plum"];
alert( fruits.length ); // 3
For instance:
// mix of values
let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ];
let fruits = [
"Apple",
"Orange",
"Plum",
];
The “trailing comma” style makes it easier to insert/remove items, because all lines become
alike.
A queue is one of the most common uses of an array. In computer science, this means an
ordered collection of elements which supports two operations:
●
push appends an element to the end.
●
shift get an element from the beginning, advancing the queue, so that the 2nd element
becomes the 1st.
shift push
In practice we need it very often. For example, a queue of messages that need to be shown on-
screen.
There’s another use case for arrays – the data structure named stack .
A stack is usually illustrated as a pack of cards: new cards are added to the top or taken from the
top:
push
pop
For stacks, the latest pushed item is received first, that’s also called LIFO (Last-In-First-Out)
principle. For queues, we have FIFO (First-In-First-Out).
Arrays in JavaScript can work both as a queue and as a stack. They allow you to add/remove
elements both to/from the beginning or the end.
pop
Extracts the last element of the array and returns it:
push
Append the element to the end of the array:
fruits.push("Pear");
shift
Extracts the first element of the array and returns it:
unshift
Add the element to the beginning of the array:
fruits.unshift('Apple');
fruits.push("Orange", "Peach");
fruits.unshift("Pineapple", "Lemon");
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. That’s essentially the same as obj[key] , where arr is
the object, while numbers are used as keys.
They extend objects providing special methods to work with ordered collections of data and also
the length property. But at the core it’s still an object.
Remember, there are only 7 basic types in JavaScript. Array is an object and thus behaves like
an object.
let arr = fruits; // copy by reference (two variables reference the same array)
…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.
fruits[99999] = 5; // assign a property with the index far greater than its length
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.
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
unshift pop
0 1 2 3
shift push
Why is it faster to work with the end of an array than with its beginning? Let’s see what happens
during the execution:
It’s not enough to take and remove the element with the number 0 . Other elements need to be
renumbered as well.
"Orange"
"Orange"
"Lemon"
"Lemon"
"Lemon"
"Apple"
"Pear"
"Pear"
"Pear"
length = 4 length = 3
clear move
0 11 2 3 1 2 3 0 1 2
elements
to the left
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 .
"Orange"
"Lemon"
"Apple"
"Apple"
"Pear"
"Pear"
length = 4 length = 3
clear
0 1 2 3 0 1 2
The pop method does not need to move anything, because other elements keep their
indexes. That’s why it’s blazingly fast.
Loops
One of the oldest ways to cycle array items is the for loop over indexes:
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.
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 only matter in bottlenecks. But still we
should be aware of the difference.
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:
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:
new Array()
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.
To evade such surprises, we usually use square brackets, unless we really know what we’re
doing.
Multidimensional arrays
Arrays can have items that are also arrays. We can use it for multidimensional arrays, for
example to store matrices:
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
toString
Arrays have their own implementation of toString method that returns a comma-separated list
of elements.
For instance:
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:
Summary
Array is a special kind of object, suited to storing and managing ordered data items.
●
The declaration:
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 will return to arrays and study more methods to add, remove, extract elements and sort
arrays in the chapter Array methods.
✔ Tasks
Is array copied?
importance: 3
// what's in fruits?
alert( fruits.length ); // ?
To solution
Array operations.
importance: 5
Jazz, Blues
Jazz, Blues, Rock-n-Roll
Jazz, Classics, Rock-n-Roll
Classics, Rock-n-Roll
Rap, Reggae, Classics, Rock-n-Roll
To solution
arr.push(function() {
alert( this );
})
arr[2](); // ?
To solution
● 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.
To solution
A maximal subarray
importance: 2
The task is: find the contiguous subarray of arr with the maximal sum of items.
For instance:
If all items are negative, it means that we take none (the subarray is empty), so the sum is zero:
Please try to think of a fast solution: O(n2) or even O(n) if you can.
To solution
Array methods
Arrays provide a lot of methods. To make things easier, in this chapter they are split into groups.
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.
splice
How to delete an element from the array?
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.
It starts from the position index : removes deleteCount elements and then inserts elem1,
..., elemN at their place. Returns the array of removed elements.
In the next example we remove 3 elements and replace them with the other two:
Here we can see that splice returns the 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 :
// from index 2
// delete 0
// then insert "complex" and "language"
arr.splice(2, 0, "complex", "language");
slice
The method arr.slice is much simpler than similar-looking arr.splice .
arr.slice([start], [end])
It returns a new array copying to it all items from index start to end (not including end ). Both
start and end can be negative, in that case position from array end is assumed.
It’s similar to a string method str.slice , but instead of substrings it makes subarrays.
For instance:
We can also call it without arguments: arr.slice() creates a copy of arr . That’s often used
to obtain a copy for further transformations that should not affect the original array.
concat
The method arr.concat creates a new array that includes values from other arrays and
additional items.
The syntax is:
arr.concat(arg1, arg2...)
The result is a new array containing items from arr , then arg1 , arg2 etc.
If an argument argN is an array, then all its elements are copied. Otherwise, the argument itself
is copied.
For instance:
// create an array from: arr and [3,4], then add values 5 and 6
alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6
Normally, it only copies elements from arrays. Other objects, even if they look like arrays, are
added as a whole:
let arrayLike = {
0: "something",
length: 1
};
let arrayLike = {
0: "something",
1: "else",
[Symbol.isConcatSpreadable]: true,
length: 2
};
Iterate: forEach
The arr.forEach method allows to run a function for every element of the array.
The syntax:
And this code is more elaborate about their positions in the target array:
The result of the function (if it returns any) is thrown away and ignored.
Searching in array
For instance:
alert( arr.indexOf(0) ); // 1
alert( arr.indexOf(false) ); // 2
alert( arr.indexOf(null) ); // -1
Note that the methods use === comparison. So, if we look for false , it finds exactly false
and not the zero.
If we want to check for inclusion, and don’t want to know the exact index, then arr.includes
is preferred.
Also, a very minor difference of includes is that it correctly handles NaN , unlike
indexOf/lastIndexOf :
The function is called for elements of the array, one after another:
●
item is the element.
●
index is its index.
●
array is the array itself.
If it returns true , the search is stopped, the item is returned. If nothing found, undefined is
returned.
For example, we have an array of users, each with the fields id and name . Let’s find the one
with id == 1 :
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
alert(user.name); // John
In real life arrays of objects is a common thing, so the find method is very useful.
Note that in the example we provide to find the function item => item.id == 1 with one
argument. That’s typical, other arguments of this function are rarely used.
The arr.findIndex method is essentially the same, but it returns the index where the element
was found instead of the element itself and -1 is returned when nothing is found.
filter
The find method looks for a single (first) element that makes the function return true .
The syntax is similar to find , but filter returns an array of all matching elements:
For instance:
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
alert(someUsers.length); // 2
Transform an array
map
The arr.map method is one of the most useful and often used.
It calls the function for each element of the array and returns the array of results.
sort(fn)
The call to arr.sort() sorts the array in place, changing its element order.
It also returns the sorted array, but the returned value is usually ignored, as arr itself is
modified.
For instance:
let arr = [ 1, 2, 15 ];
Literally, all elements are converted to strings for comparisons. For strings, lexicographic ordering
is applied and indeed "2" > "15" .
To use our own sorting order, we need to supply a function as the argument of arr.sort() .
function compare(a, b) {
if (a > b) return 1; // if the first value is greater than the second
if (a == b) return 0; // if values are equal
if (a < b) return -1; // if the first value is less than the second
}
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
Let’s step aside and think what’s happening. The arr can be array of anything, right? It may
contain numbers or strings or objects or whatever. We have a set of some items. To sort it, we
need an ordering function that knows how to compare its elements. The default is a string order.
The arr.sort(fn) method implements a generic sorting algorithm. We don’t need to care
how it internally works (an optimized quicksort 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:
The algorithm may compare an element with multiple others in the process, but it tries to make as
few comparisons as possible.
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:
let arr = [ 1, 2, 15 ];
alert(arr); // 1, 2, 15
reverse
The method arr.reverse reverses the order of elements in arr .
For instance:
The str.split(delim) method does exactly that. It splits the string into an array by the given
delimiter delim .
The call arr.join(glue) does the reverse to split . It creates a string of arr items joined by
glue between them.
For instance:
reduce/reduceRight
When we need to iterate over an array – we can use forEach , for or for..of .
When we need to iterate and return the data for each element – we can use map .
The methods arr.reduce and arr.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 function is applied to all array elements one after another and “carries on” its result to the
next call.
Arguments:
● previousValue – is the result of the previous function call, equals initial the first time
(if initial is provided).
●
item – is the current array item.
●
index – is its position.
●
array – is the array.
As function is applied, the result of the previous function call is passed to the next one as the first
argument.
Sounds complicated, but it’s not if you think about the first argument as the “accumulator” that
stores the combined result of all previous execution. And at the end it becomes the result of
reduce .
alert(result); // 15
The function passed to reduce uses only 2 arguments, that’s typically enough.
1 2 3 4 5 0+1+2+3+4+5 = 15
Or in the form of a table, where each row represents a function call on the next array element:
Here we can clearly see how the result of the previous call becomes the first argument of the next
one.
We also can omit the initial value:
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:
The method arr.reduceRight does the same, but goes from right to left.
Array.isArray
Arrays do not form a separate language type. They are based on objects.
…But arrays are used so often that there’s a special method for that: Array.isArray(value) . It
returns true if the value is an array, and false otherwise.
alert(Array.isArray({})); // false
alert(Array.isArray([])); // true
Almost all array methods that call functions – like find , filter , map , with a notable
exception of sort , accept an optional additional parameter thisArg .
That parameter is not explained in the sections above, because it’s rarely used. But for
completeness we have to cover it.
arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// thisArg is the optional last argument
For example, here we use a method of army object as a filter, and thisArg passes the
context:
let army = {
minAge: 18,
maxAge: 27,
canJoin(user) {
return user.age >= this.minAge && user.age < this.maxAge;
}
};
let users = [
{age: 16},
{age: 20},
{age: 23},
{age: 30}
];
alert(soldiers.length); // 2
alert(soldiers[0].age); // 20
alert(soldiers[1].age); // 23
Summary
Please note that methods sort , reverse and splice modify the array itself.
These methods are the most used ones, they cover 99% of use cases. But there are few others:
● arr.some(fn) /arr.every(fn) 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) – fills the array with repeating value from index start to end .
●
arr.copyWithin(target, start, end) – copies its elements from position start till position
end into itself, at position target (overwrites existing).
From the first sight it may seem that there are so many methods, quite difficult to remember. But
actually that’s much easier.
Look through the cheat sheet just to be aware of them. Then solve the tasks of this chapter to
practice, so that you have experience with array methods.
Afterwards whenever you need to do something with an array, and you don’t know how – come
here, look at the cheat sheet and find the right method. Examples will help you to write it
correctly. Soon you’ll automatically remember the methods, without specific efforts from your
side.
✔ Tasks
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:
camelize("background-color") == 'backgroundColor';
camelize("list-style-image") == 'listStyleImage';
camelize("-webkit-transition") == 'WebkitTransition';
P.S. Hint: use split to split the string into an array, transform it and join back.
To solution
Filter range
importance: 4
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:
To solution
Filter range "in place"
importance: 4
The function should only modify the array. It should not return anything.
For instance:
To solution
To solution
We have an array of strings arr . We’d like to have a sorted copy of it, but keep arr
unmodified.
To solution
Create an extendable calculator
importance: 5
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:
2.
Then add the method addMethod(name, func) that teaches the calculator a new
operation. It takes the operator name and the two-argument function func(a,b) that
implements it.
To solution
Map to names
importance: 5
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:
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };
To solution
Map to objects
importance: 5
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:
/*
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.
To solution
Write the function sortByAge(users) that gets an array of objects with the age property and
sorts them by age .
For instance:
sortByAge(arr);
To solution
Shuffle an array
importance: 3
Write the function shuffle(array) that shuffles (randomly reorders) elements of the array.
Multiple runs of shuffle may lead to different orders of elements. For instance:
shuffle(arr);
// arr = [3, 2, 1]
shuffle(arr);
// arr = [2, 1, 3]
shuffle(arr);
// arr = [3, 1, 2]
// ...
All element orders should have an equal probability. For instance, [1,2,3] can be reordered
as [1,2,3] or [1,3,2] or [3,1,2] etc, with equal probability of each case.
To solution
Write the function getAverageAge(users) that gets an array of objects with property age
and returns the average age.
For instance:
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 29 };
To solution
Create a function unique(arr) that should return an array with unique items of arr .
For instance:
function unique(arr) {
/* your code */
}
To solution
Iterables
Iterable objects is a generalization of arrays. That’s a concept that allows to make any object
useable in a for..of loop.
Of course, Arrays are iterable. But there are many other built-in objects, that are iterable as well.
For instance, strings are also iterable.
If an object isn’t technically an array, but represents a collection (list, set) of something, then
for..of is a great syntax to loop over it, so let’s see how to make it work.
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 .
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).
1. When for..of starts, it calls that method once (or errors if not found). The method must
return an iterator – an object with the method next .
2. Onward, for..of works only with that returned object.
3. When for..of wants the next value, it calls next() on that object.
4. The result of next() must have the form {done: Boolean, value: any} , where
done=true means that the iteration is finished, otherwise value is the next value.
let range = {
from: 1,
to: 5
};
// now it works!
for (let num of range) {
alert(num); // 1, then 2, 3, 4, 5
}
So, the iterator object is separate from the object it iterates over.
Technically, we may merge them and use range itself as the iterator to make the code simpler.
Like this:
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 };
}
}
};
Now range[Symbol.iterator]() returns the range object itself: it has the necessary
next() method and remembers the current iteration progress in this.current . Shorter?
Yes. And sometimes that’s fine too.
The downside is that now it’s impossible to have two for..of loops running over the object
simultaneously: they’ll share the iteration state, because there’s only one iterator – the object
itself. But two parallel for-ofs is a rare thing, even in async scenarios.
Infinite iterators
Infinite iterators are also possible. For instance, the range becomes infinite for range.to
= Infinity . Or we can make an iterable object that generates an infinite sequence of
pseudorandom numbers. Also can be useful.
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
We’ll iterate over a string in exactly the same way as for..of , but with direct calls. This code
creates a string iterator and gets values from it “manually”:
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 over the process than for..of . For instance,
we can split the iteration process: iterate a bit, then stop, do something else, and then resume
later.
There are two official terms that look similar, but are very different. Please make sure you
understand them well to avoid the confusion.
●
Iterables are objects that implement the Symbol.iterator method, as described above.
●
Array-likes are objects that have indexes and length , so they look like arrays.
When we use JavaScript for practical tasks in browser or other environments, we may meet
objects that are iterables or array-likes, or both.
For instance, strings are both iterable ( for..of works on them) and array-like (they have
numeric indexes and length ).
But an iterable may be not array-like. And vice versa an array-like may be not iterable.
For example, the range in the example above is iterable, but not array-like, because it does not
have indexed properties and length .
Both iterables and array-likes are usually not arrays, they don’t have push , pop etc. That’s
rather inconvenient if we have such an object and want to work with it as with an array. E.g. we
would like to work with range using array methods. How to achieve that?
Array.from
There’s a universal method Array.from that takes an iterable or array-like value and makes a
“real” Array from it. Then we can call array methods on it.
For instance:
let arrayLike = {
0: "Hello",
1: "World",
length: 2
};
Array.from at the line (*) takes the object, examines it for being an iterable or array-like,
then makes a new array and copies all items to it.
The full syntax for Array.from allows to provide an optional “mapping” function:
The optional second argument mapFn can be a function that will be applied to each element
before adding to the array, and thisArg allows to set this for it.
For instance:
alert(arr); // 1,4,9,16,25
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.
alert(chars);
…But it is shorter.
alert( slice(str, 1, 3) ); // 😂𩷶
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 the 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.
But that’s not enough for real life. That’s why Map and Set also exist.
Map
Map is a collection of keyed data items, just like an Object . But the main difference is that
Map allows keys of any type.
For instance:
let map = new Map();
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:
Using objects as keys is one of most notable and important Map features. For string keys,
Object can be fine, but not for object keys.
Let’s try:
As visitsCountObj is an object, it converts all keys, such as john to strings, so we’ve got
the string key "[object Object]" . Definitely not what we want.
map.set('1', 'str1')
.set(1, 'num1')
.set(true, 'bool1');
For instance:
When a Map is created, we can pass an array (or another iterable) with key/value pairs for
initialization, like this:
If we have a plain object, and we’d like to create a Map from it, then we can use built-in method
Object.entries(obj) that returns an array of key/value pairs for an object exactly in that format.
let obj = {
name: "John",
age: 30
};
We’ve just seen how to create Map from a plain object with Object.entries(obj) .
There’s Object.fromEntries method that does the reverse: given an array of [key,
value] pairs, it creates an object from them:
alert(prices.orange); // 2
We can use Object.fromEntries to get an plain object from Map .
E.g. we store the data in a Map , but we need to pass it to a 3rd-party code that expects a plain
object.
Here we go:
// done!
// obj = { banana: 1, orange: 2, meat: 4 }
alert(obj.orange); // 2
A call to map.entries() returns an array of key/value pairs, exactly in the right format for
Object.fromEntries .
That’s the same, because Object.fromEntries expects an iterable object as the argument.
Not necessarily an array. And the standard iteration for map returns same key/value pairs as
map.entries() . So we get a plain object with same key/values as the map .
Set
A Set is a special type collection – “set of values” (without keys), where each value may occur
only once.
The main feature is that repeated calls of set.add(value) with the same value don’t do
anything. That’s the reason why each value appears in a Set only once.
For example, we have visitors coming, and we’d like to remember everyone. But repeated visits
should not lead to duplicates. A visitor must be “counted” only once.
Set is just the right thing for that:
The alternative to Set could be an array of users, and the code to check for duplicates on every
insertion using arr.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.
Note the funny thing. The callback function passed in forEach has 3 arguments: a value ,
then the same value valueAgain , and then the target object. Indeed, the same value appears
in the arguments twice.
That’s for compatibility with Map where the callback passed forEach has three arguments.
Looks a bit strange, for sure. But may help to replace Map with Set in certain cases with ease,
and vice versa.
The same methods Map has for iterators are also supported:
●
set.keys() – returns an iterable object for values,
●
set.values() – same as set.keys() , for compatibility with Map ,
● set.entries() – returns an iterable object for entries [value, value] , exists for
compatibility with Map .
Summary
Iteration over Map and Set is always in the insertion order, so we can’t say that these
collections are unordered, but we can’t reorder elements or directly get an element by its number.
✔ Tasks
Create a function unique(arr) that should return an array with unique items of arr .
For instance:
function unique(arr) {
/* your code */
}
P.S. Here strings are used, but can be values of any type.
To solution
Filter anagrams
importance: 4
Anagrams are words that have the same number of same letters, but in different order.
For instance:
nap - pan
ear - are - era
cheaters - hectares - teachers
For instance:
From every anagram group should remain only one word, no matter which one.
To solution
Iterable keys
importance: 5
We’d like to get an array of map.keys() in a variable and then do apply array-specific methods
to it, e.g. .push .
map.set("name", "John");
To solution
For instance:
Usually, properties of an object or elements of an array or another data structure are considered
reachable and kept in memory while that data structure is in memory.
For instance, if we put an object into an array, then while the array is alive, the object will be alive
as well, even if there are no other references to it.
Like this:
Similar to that, if we use an object as the key in a regular Map , then while the Map exists, that
object exists as well. It occupies memory and may not be garbage collected.
For instance:
let john = { name: "John" };
WeakMap
The first difference from Map is that WeakMap keys must be objects, not primitive values:
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.
Compare it with the regular Map example above. Now if john only exists as the key of
WeakMap – it will be automatically deleted from the map (and memory).
WeakMap does not support iteration and methods keys() , values() , entries() , so
there’s no way to get all keys or values from it.
Why such a limitation? That’s for technical reasons. If an object has lost all other references (like
john in the code above), then it is to be garbage-collected automatically. But technically it’s not
exactly specified when the cleanup happens.
The JavaScript engine decides that. It may choose to perform the memory cleanup immediately
or to wait and do the cleaning later when more deletions happen. So, technically the current
element count of a WeakMap is not known. The engine may have cleaned it up or not, or did it
partially. For that reason, methods that access all keys/values are not supported.
If we’re working with an object that “belongs” to another code, maybe even a third-party library,
and would like to store some data associated with it, that should only exist while the object is
alive – then WeakMap is exactly what’s needed.
We put the data to a WeakMap , using the object as the key, and when the object is garbage
collected, that data will automatically disappear as well.
For instance, we have code that keeps a visit count for users. The information is stored in a map:
a user object is the key and the visit count is the value. When a user leaves (its object gets
garbage collected), we don’t want to store their visit count anymore.
// visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count
And here’s another part of the code, maybe another file using it:
// main.js
let john = { name: "John" };
Now john object should be garbage collected, but remains in memory, as it’s a key in
visitsCountMap .
We need to clean visitsCountMap when we remove users, otherwise it will grow in memory
indefinitely. Such cleaning can become a tedious task in complex architectures.
// visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count
Now we don’t have to clean visitsCountMap . After john object becomes unreachable by all
means except as a key of WeakMap , it gets removed from memory, along with the information by
that key from WeakMap .
Another common example is caching: when a function result should be remembered (“cached”),
so that future calls on the same object reuse it.
// cache.js
let cache = new Map();
cache.set(obj, result);
}
return cache.get(obj);
}
// main.js
let obj = {/* let's say we have an object */};
For multiple calls of process(obj) with the same object, it only calculates the result the first
time, and then just takes it from cache . The downside is that we need to clean cache when
the object is not needed any more.
If we replace Map with WeakMap , then this problem disappears: the cached result will be
removed from memory automatically after the object gets garbage collected.
// cache.js
let cache = new WeakMap();
cache.set(obj, result);
}
return cache.get(obj);
}
// main.js
let obj = {/* some object */};
WeakSet
Being “weak”, it also serves as an additional storage. But not for an arbitrary data, but rather for
“yes/no” facts. A membership in WeakSet may mean something about the object.
For instance, we can add users to WeakSet to keep track of those who visited our site:
let visitedSet = new WeakSet();
john = null;
The most notable limitation of WeakMap and WeakSet is the absence of iterations, and inability
to get all current content. That may appear inconvenient, but does not prevent
WeakMap/WeakSet from doing their main job – be an “additional” storage of data for objects
which are stored/managed at another place.
Summary
WeakMap is Map -like collection that allows only objects as keys and removes them together
with associated value once they become inaccessible by other means.
WeakSet is Set -like collection that stores only objects and removes them once they become
inaccessible by other means.
Both of them do not support methods and properties that refer to all keys or their count. Only
individual operations are allowed.
WeakMap and WeakSet are used as “secondary” data structures in addition to the “main”
object storage. Once the object is removed from the main storage, if it is only found as the key of
WeakMap or in a WeakSet , it will be cleaned up automatically.
✔ Tasks
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
Your code can access it, but the messages are managed by someone else’s code. New
messages are added, old ones are removed regularly by that code, and you don’t know the exact
moments when it happens.
Now, which data structure could you use to store information about whether the message “has
been read”? The structure must be well-suited to give the answer “was it read?” for the given
message object.
P.S. When a message is removed from messages , it should disappear from your structure as
well.
P.P.S. We shouldn’t modify message objects, add our properties to them. As they are managed
by someone else’s code, that may lead to bad consequences.
To solution
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
The question now is: which data structure you’d suggest to store the information: “when the
message was read?”.
In the previous task we only needed to store the “yes/no” fact. Now we need to store the date,
and it should only remain in memory until the message is garbage collected.
P.S. Dates can be stored as objects of built-in Date class, that we’ll cover later.
To solution
These methods are generic, there is a common agreement to use them for data structures. If we
ever create a data structure of our own, we should implement them too.
They are supported for:
●
Map
●
Set
● Array
Plain objects also support similar methods, but the syntax is a bit different.
Map Object
The first difference is that we have to call Object.keys(obj) , and not obj.keys() .
Why so? The main reason is flexibility. Remember, objects are a base of all complex structures in
JavaScript. So we may have an object of our own like data that implements its own
data.values() method. And we still can call Object.values(data) on it.
The second difference is that Object.* methods return “real” array objects, not just an iterable.
That’s mainly for historical reasons.
For instance:
let user = {
name: "John",
age: 30
};
●
Object.keys(user) = ["name", "age"]
●
Object.values(user) = ["John", 30]
● Object.entries(user) = [ ["name","John"], ["age",30] ]
let user = {
name: "John",
age: 30
};
Usually that’s convenient. But if we want symbolic keys too, then there’s a separate method
Object.getOwnPropertySymbols that returns an array of only symbolic keys. Also, there
exist a method Reflect.ownKeys(obj) that returns all keys.
Transforming objects
Objects lack many methods that exist for arrays, e.g. map , filter and others.
For example, we have an object with prices, and would like to double them:
let prices = {
banana: 1,
orange: 2,
meat: 4,
};
alert(doublePrices.meat); // 8
It may look difficult from the first sight, but becomes easy to understand after you use it once or
twice. We can make powerful chains of transforms this way.
✔ Tasks
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:
let salaries = {
"John": 100,
"Pete": 300,
"Mary": 250
};
To solution
Count properties
importance: 5
Write a function count(obj) that returns the number of properties in the object:
let user = {
name: 'John',
age: 30
};
alert( count(user) ); // 2
To solution
Destructuring assignment
The two most used data structures in JavaScript are Object and Array .
Objects allow us to create a single entity that stores data items by key, and arrays allow us to
gather data items into an ordered collection.
But when we pass those to a function, it may need not an object/array as a whole, but rather
individual pieces.
Destructuring assignment is a special syntax that allows us to “unpack” arrays or objects into a
bunch of variables, as sometimes that’s more convenient. Destructuring also works great with
complex functions that have a lot of parameters, default values, and so on.
Array destructuring
An example of how the array is destructured into variables:
// destructuring assignment
// sets firstName = arr[0]
// and surname = arr[1]
let [firstName, surname] = arr;
alert(firstName); // Ilya
alert(surname); // Kantor
In the code above, the second element of the array is skipped, the third one is assigned to
title , and the rest of the array items is also skipped (as there are no variables for them).
Works with any iterable on the right-side
…Actually, we can use it with any iterable, not only arrays:
alert(user.name); // Ilya
let user = {
name: "John",
age: 30
};
alert(name1); // Julius
alert(name2); // Caesar
The value of rest is the array of the remaining array elements. We can use any other variable
name in place of rest , just make sure it has three dots before it and goes last in the
destructuring assignment.
Default values
If there are fewer values in the array than variables in the assignment, there will be no error.
Absent values are considered undefined:
alert(firstName); // undefined
alert(surname); // undefined
If we want a “default” value to replace the missing one, we can provide it using = :
// default values
let [name = "Guest", surname = "Anonymous"] = ["Julius"];
Default values can be more complex expressions or even function calls. They are evaluated only
if the value is not provided.
For instance, here we use the prompt function for two defaults. But it will run only for the
missing one:
Object destructuring
We have an existing object at the right side, that we want to split into variables. The left side
contains a “pattern” for corresponding properties. In the simple case, that’s a list of variable
names in {...} .
For instance:
let options = {
title: "Menu",
width: 100,
height: 200
};
alert(title); // Menu
alert(width); // 100
alert(height); // 200
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:
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:
let options = {
title: "Menu"
};
alert(title); // Menu
alert(width); // 100
alert(height); // 200
Just like with arrays or function parameters, default values can be any expressions or even
function calls. They will be evaluated if the value is not provided.
In the code below prompt asks for width , but not for title :
let options = {
title: "Menu"
};
alert(title); // Menu
alert(width); // (whatever the result of prompt is)
let options = {
title: "Menu"
};
alert(title); // Menu
alert(w); // 100
alert(h); // 200
If we have a complex object with many properties, we can extract only what we need:
let options = {
title: "Menu",
width: 100,
height: 200
};
alert(title); // Menu
The rest pattern “…”
What if the object has more properties than we have variables? Can we take some and then
assign the “rest” somewhere?
We can use the rest pattern, just like we did with arrays. It’s not supported by some older
browsers (IE, use Babel to polyfill it), but works in modern ones.
let options = {
title: "Menu",
height: 200,
width: 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:
{
// a code block
let message = "Hello";
// ...
alert( message );
}
So here JavaScript assumes that we have a code block, that’s why there’s an error. We want
destructuring instead.
To show JavaScript that it’s not a code block, we can wrap the expression in parentheses
(...) :
// okay now
({title, width, height} = {title: "Menu", width: 200, height: 100});
Nested destructuring
If an object or an array contain other nested objects and arrays, we can use more complex left-
side patterns to extract deeper portions.
In the code below options has another object in the property size and an array in the
property items . The pattern at the left side of the assignment has the same structure to extract
values from them:
let options = {
size: {
width: 100,
height: 200
},
items: ["Cake", "Donut"],
extra: true
};
alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut
All properties of options object except extra that is absent in the left part, are assigned to
corresponding variables:
Finally, we have width , height , item1 , item2 and title from the default value.
Note that there are no variables for size and items , as we take their content instead.
There are times when a function has many parameters, most of which are optional. That’s
especially true for user interfaces. Imagine a function that creates a menu. It may have a width, a
height, a title, items list and so on.
In real-life, the problem is how to remember the order of arguments. Usually IDEs try to help us,
especially if the code is well-documented, but still… Another problem is how to call a function
when most parameters are ok by default.
Like this?
// undefined where default values are fine
showMenu("My Menu", undefined, undefined, ["Item1", "Item2"])
That’s ugly. And becomes unreadable when we deal with more parameters.
We can pass parameters as an object, and the function immediately destructurizes them into
variables:
showMenu(options);
We can also use more complex destructuring with nested objects and colon mappings:
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);
function({
incomingProperty: varName = defaultValue
...
})
Then, for an object of parameters, there will be a variable varName for property
incomingProperty , with defaultValue by default.
Please note that such destructuring assumes that showMenu() does have an argument. If we
want all values by default, then we should specify an empty object:
We can fix this by making {} the default value for the whole object of parameters:
In the code above, the whole arguments object is {} by default, so there’s always something to
destructurize.
Summary
●
Destructuring assignment allows for instantly mapping an object or array onto many variables.
●
The full object syntax:
This means that property prop should go into the variable varName and, if no such property
exists, then the default value should be used.
Object properties that have no mapping are copied to the rest object.
●
The full array syntax:
The first item goes to item1 ; the second goes into item2 , all the rest makes the array
rest .
● It’s possible to extract data from nested arrays/objects, for that the left side must have the
same structure as the right one.
✔ Tasks
Destructuring assignment
importance: 5
We have an object:
let user = {
name: "John",
years: 30
};
To solution
let salaries = {
"John": 100,
"Pete": 300,
"Mary": 250
};
Create the function topSalary(salaries) that returns the name of the top-paid person.
To solution
Date and time
Let’s meet a new built-in object: Date . It stores the date, time and provides methods for
date/time management.
For instance, we can use it to store creation/modification times, to measure time, or just to print
out the current date.
Creation
To create a new Date object call new Date() with one of the following arguments:
new Date()
Without arguments – create a Date object for the current date and time:
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.
An integer number representing the number of milliseconds that has passed since the beginning
of 1970 is called a timestamp.
It’s a lightweight numeric representation of a date. We can always create a date from a
timestamp using new Date(timestamp) and convert the existing Date object to a
timestamp using the date.getTime() method (see below).
// 31 Dec 1969
let Dec31_1969 = new Date(-24 * 3600 * 1000);
alert( Dec31_1969 );
new Date(datestring)
If there is a single argument, and it’s a string, then it is parsed automatically. The algorithm is the
same as Date.parse uses, we’ll cover it later.
let date = new Date("2017-01-26");
alert(date);
// The time is not set, so it's assumed to be midnight GMT and
// is adjusted according to the timezone the code is run in
// So the result could be
// Thu Jan 26 2017 11:00:00 GMT+1100 (Australian Eastern Daylight Time)
// or
// Wed Jan 25 2017 16:00:00 GMT-0800 (Pacific Standard Time)
For instance:
There are methods to access the year, month and so on from the Date object:
getFullYear()
getMonth()
getDate()
Get the day of month, from 1 to 31, the name of the method does look a little bit strange.
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:
// current date
let date = new Date();
// 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:
●
setMonth(month, [date])
●
setDate(date)
●
setHours(hour, [min], [sec], [ms])
●
setMinutes(min, [sec], [ms])
●
setSeconds(sec, [ms])
● setMilliseconds(ms)
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:
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:
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”:
date.setDate(0); // min day is 1, so the last day of the previous month is assumed
alert( date ); // 31 Dec 2015
The important side effect: dates can be subtracted, the result is their difference in ms.
// do the job
for (let i = 0; i < 100000; i++) {
let doSomething = i * i * i;
}
Date.now()
It is used mostly for convenience or when performance matters, like in games in JavaScript or
other specialized applications.
// do the job
for (let i = 0; i < 100000; i++) {
let doSomething = i * i * i;
}
alert( `The loop took ${end - start} ms` ); // subtract numbers, not dates
Benchmarking
For instance, let’s measure two functions that calculate the difference between two dates: which
one is faster?
// we have date1 and date2, which function faster returns their difference in ms?
function diffSubtract(date1, date2) {
return date2 - date1;
}
// or
function diffGetTime(date1, date2) {
return date2.getTime() - date1.getTime();
}
These two do exactly the same thing, but one of them uses an explicit date.getTime() to get
the date in ms, and the other one relies on a date-to-number transform. Their result is always the
same.
The first idea may be to run them many times in a row and measure the time difference. For our
case, functions are very simple, so we have to do it at least 100000 times.
Let’s measure:
function bench(f) {
let date1 = new Date(0);
let date2 = new Date();
Wow! Using getTime() is so much faster! That’s because there’s no type conversion, it is
much easier for engines to optimize.
Imagine that at the time of running bench(diffSubtract) CPU was doing something in
parallel, and it was taking resources. And by the time of running bench(diffGetTime) that
work has finished.
As a result, the first benchmark will have less CPU resources than the second. That may lead to
wrong results.
For more reliable benchmarking, the whole pack of benchmarks should be rerun multiple
times.
For example, like this:
function bench(f) {
let date1 = new Date(0);
let date2 = new Date();
let time1 = 0;
let time2 = 0;
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:
// added for "heating up" prior to the main loop
bench(diffSubtract);
bench(diffGetTime);
// now benchmark
for (let i = 0; i < 10; i++) {
time1 += bench(diffSubtract);
time2 += bench(diffGetTime);
}
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:
let ms = Date.parse('2012-01-26T13:51:50.417-07:00');
alert(date);
Summary
●
Date and time in JavaScript are represented with the Date object. We can’t create “only
date” or “only time”: Date objects always carry both.
●
Months are counted from zero (yes, January is a zero month).
●
Days of week in getDay() are also counted from zero (that’s Sunday).
●
Date auto-corrects itself when out-of-range components are set. Good for adding/subtracting
days/months/hours.
●
Dates can be subtracted, giving their difference in milliseconds. That’s because a Date
becomes the timestamp when converted to a number.
●
Use Date.now() to get the current timestamp fast.
Note that unlike many other systems, timestamps in JavaScript are in milliseconds, not in
seconds.
Sometimes we need more precise time measurements. JavaScript itself does not have a way to
measure time in microseconds (1 millionth of a second), but most environments provide it. For
instance, browser has performance.now() that gives the number of milliseconds from the start
of page loading with microsecond precision (3 digits after the point):
Node.js has microtime module and other ways. Technically, almost any device and
environment allows to get more precision, it’s just not in Date .
✔ Tasks
Create a date
importance: 5
Create a Date object for the date: Feb 20, 2012, 3:12am. The time zone is local.
To solution
Show a weekday
importance: 5
Write a function getWeekDay(date) to show the weekday in short format: ‘MO’, ‘TU’, ‘WE’,
‘TH’, ‘FR’, ‘SA’, ‘SU’.
For instance:
let date = new Date(2012, 0, 3); // 3 Jan 2012
alert( getWeekDay(date) ); // should output "TU"
To solution
European weekday
importance: 5
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 .
To solution
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.
To solution
Parameters:
To solution
Write a function getSecondsToday() that returns the number of seconds from the beginning
of today.
For instance, if now were 10:00 am , and there was no daylight savings shift, then:
The function should work in any day. That is, it should not have a hard-coded value of “today”.
To solution
getSecondsToTomorrow() == 3600
P.S. The function should work at any day, the “today” is not hardcoded.
To solution
For instance:
To solution
let user = {
name: "John",
age: 30,
toString() {
return `{name: "${this.name}", age: ${this.age}}`;
}
};
…But in the process of development, new properties are added, old properties are renamed and
removed. Updating such toString every time can become a pain. We could try to loop over
properties in it, but what if the object is complex and has nested objects in properties? We’d need
to implement their conversion as well.
Luckily, there’s no need to write the code to handle all this. The task has been solved already.
JSON.stringify
The JSON (JavaScript Object Notation) is a general format to represent values and objects. It
is described as in RFC 4627 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.
let student = {
name: 'John',
age: 30,
isAdmin: false,
courses: ['html', 'css', 'js'],
wife: null
};
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.
Please note that a JSON-encoded object has several important differences from the object literal:
● Strings use double quotes. No single quotes or backticks in JSON. So 'John' becomes
"John" .
●
Object property names are double-quoted also. That’s obligatory. So age:30 becomes
"age":30 .
For instance:
Namely:
●
Function properties (methods).
●
Symbolic properties.
●
Properties that store undefined .
let user = {
sayHi() { // ignored
alert("Hello");
},
[Symbol("id")]: 123, // ignored
something: undefined // ignored
};
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:
let meetup = {
title: "Conference",
room: {
number: 23,
participants: ["john", "ann"]
}
};
alert( JSON.stringify(meetup) );
/* The whole structure is stringified:
{
"title":"Conference",
"room":{"number":23,"participants":["john","ann"]},
}
*/
For instance:
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: ["john", "ann"]
};
number: 23
occupiedBy place
title: "Conference"
participants
...
value
A value to encode.
replacer
Array of properties to encode or a mapping function function(key, value) .
space
Amount of space to use for formatting
Most of the time, JSON.stringify is used with the first argument only. But if we need to fine-
tune the replacement process, like to filter out circular references, we can use the second
argument of JSON.stringify .
For instance:
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup references room
};
Here we are probably too strict. The property list is applied to the whole object structure. So the
objects in participants are empty, because name is not in the list.
Let’s include in the list every property except room.occupiedBy that would cause the circular
reference:
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup references room
};
Now everything except occupiedBy is serialized. But the list of properties is quite long.
In our case, we can return value “as is” for everything except occupiedBy . To ignore
occupiedBy , the code below returns undefined :
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup references room
};
Please note that replacer function gets every key/value pair including nested objects and
array items. It is applied recursively. The value of this inside replacer is the object that
contains the current property.
The first call is special. It is made using a special “wrapper object”: {"": meetup} . In other
words, the first (key, value) pair has an empty key, and the value is the target object as a
whole. That’s why the first line is ":[object Object]" in the example above.
The idea is to provide as much power for replacer as possible: it has a chance to analyze and
replace/skip even the whole object if necessary.
Formatting: space
Previously, all stringified objects had no indents and extra spaces. That’s fine if we want to send
an object over a network. The space argument is used exclusively for a nice output.
Here space = 2 tells JavaScript to show nested objects on multiple lines, with indentation of 2
spaces inside an object:
let user = {
name: "John",
age: 25,
roles: {
isAdmin: false,
isEditor: true
}
};
The space parameter is used solely for logging and nice-output purposes.
Custom “toJSON”
Like toString for string conversion, an object may provide method toJSON for to-JSON
conversion. JSON.stringify automatically calls it if available.
For instance:
let room = {
number: 23
};
let meetup = {
title: "Conference",
date: new Date(Date.UTC(2017, 0, 1)),
room
};
alert( JSON.stringify(meetup) );
/*
{
"title":"Conference",
"date":"2017-01-01T00:00:00.000Z", // (1)
"room": {"number":23} // (2)
}
*/
Here we can see that date (1) became a string. That’s because all dates have a built-in
toJSON method which returns such kind of string.
Now let’s add a custom toJSON for our object room (2) :
let room = {
number: 23,
toJSON() {
return this.number;
}
};
let meetup = {
title: "Conference",
room
};
alert( JSON.stringify(room) ); // 23
alert( JSON.stringify(meetup) );
/*
{
"title":"Conference",
"room": 23
}
*/
As we can see, toJSON is used both for the direct call JSON.stringify(room) and when
room is nested in another encoded object.
JSON.parse
The syntax:
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:
// stringified array
let numbers = "[0, 1, 2, 3]";
numbers = JSON.parse(numbers);
alert( numbers[1] ); // 1
let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';
alert( user.friends[1] ); // 1
The JSON may be as complex as necessary, objects and arrays can include other objects and
arrays. But they must obey the same JSON format.
Here are typical mistakes in hand-written JSON (sometimes we have to write it for debugging
purposes):
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 , 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
…And now we need to deserialize it, to turn back into JavaScript object.
Let’s do it by calling JSON.parse :
Whoops! An error!
The value of meetup.date is a string, not a Date object. How could JSON.parse know that
it should transform that string into a Date ?
Let’s pass to JSON.parse the reviving function as the second argument, that returns all values
“as is”, but date will become a Date :
let schedule = `{
"meetups": [
{"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
{"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
]
}`;
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 to serialize into JSON and 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 .
✔ Tasks
Turn the user into JSON and then read it back into another variable.
let user = {
name: "John Smith",
age: 35
};
To solution
Exclude backreferences
importance: 5
In simple cases of circular references, we can exclude an offending property from serialization by
its name.
But sometimes we can’t just use the name, as it may be used both in circular references and
normal properties. So we can check the property by its value.
Write replacer function to stringify everything, but remove properties that reference meetup :
let room = {
number: 23
};
let meetup = {
title: "Conference",
occupiedBy: [{name: "John"}, {name: "Alice"}],
place: room
};
// circular references
room.occupiedBy = meetup;
meetup.self = meetup;
To solution
Advanced working with functions
Recursion and stack
Let’s return to functions and study them more in-depth.
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.
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.
pow(2, 2) = 4
pow(2, 3) = 8
pow(2, 4) = 16
function pow(x, n) {
let result = 1;
return result;
}
alert( pow(2, 3) ); // 8
function pow(x, n) {
if (n == 1) {
return x;
} else {
return x * pow(x, n - 1);
}
}
alert( pow(2, 3) ); // 8
if n==1 = x
/
pow(x, n) =
\
else = x * pow(x, n - 1)
pow(x,n)
No
n == 1 ? x * pow(x, n-1)
Yes
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.
Recursion is usually shorter
A recursive solution is usually shorter than an iterative one.
Here we can rewrite the same using the conditional operator ? instead of if to make
pow(x, n) more terse and still very readable:
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 rely on it being 10000,
some engines allow more, but 100000 is probably out of limit for the majority of them. There are
automatic optimizations that help alleviate this (“tail calls optimizations”), but they are not yet
supported everywhere and work only in simple cases.
That limits the application of recursion, but it still remains very wide. There are many tasks where
recursive way of thinking gives simpler code, easier to maintain.
Now let’s examine how recursive calls work. For that we’ll look under the hood of functions.
The information about the process of execution of a running function is stored in its execution
context.
The execution context 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.
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.
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 } call: pow(2, 3)
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.
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.
Please note:
Here in the picture we use the word “line”, as our example there’s only one subcall in line, but
generally a single line of code may contain multiple subcalls, like pow(…) + pow(…) +
somethingElse(…) .
So it would be more precise to say that the execution resumes “immediately after the subcall”.
pow(2, 1)
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:
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:
function pow(x, n) {
if (n == 1) {
return x;
} else {
return x * pow(x, n - 1);
}
}
As the function finishes, its execution context is not needed anymore, so it’s removed from the
memory. The previous one is restored off the top of the stack:
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 .
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 .
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
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
}]
}
};
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? If we
put 3-4 nested subloops in the code to traverse a single object, it becomes rather ugly.
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 1st case is the base of recursion, the trivial case, when we get an array.
The 2nd case when we get an object is the recursive step. A complex task is split into subtasks
for smaller departments. They may in turn split again, but sooner or later the split will finish at (1).
The code is short and easy to understand (hopefully?). That’s the power of recursion. It also
works for any level of subdepartment nesting.
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 Array methods 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
For web-developers there are much better-known examples: HTML and XML documents.
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.
…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, when we
have to work with the beginning.
Alternatively, if we really need fast insertion/deletion, we can choose another data structure called
a linked list .
For instance:
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
Here we can even more clearer see that there are multiple objects, each one has the value
and next pointing to the neighbour. The list variable is the first object in the chain, so
following next pointers from it we can reach any element.
The list can be easily split into multiple parts and later joined back:
value value
next next
list 1 2 null
value value
next next
secondList 3 4 null
To join:
list.next.next = secondList;
To remove a value from the middle, change next of the previous one:
list.next = list.next.next;
value value value value
next next next next
list "new item" 2 3 4 null
value
next
1
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.
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 – the ordered structure that must allow very fast adding/removing elements from both
ends, but access to its middle is not needed.
Summary
Terms:
●
Recursion is a programming term that means calling a function from itself. Recursive functions
can be used to solve tasks in elegant ways.
When a function calls itself, that’s called a recursion step. The basis of recursion is function
arguments that make the task so simple that the function does not make further calls.
●
A recursively-defined 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).
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.
✔ Tasks
For instance:
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
To solution
Calculate factorial
importance: 4
The 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!
n! = n * (n - 1) * (n - 2) * ...*1
The task is to write a function factorial(n) that calculates n! using recursive calls.
To solution
Fibonacci numbers
importance: 5
The sequence of Fibonacci numbers 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 and many natural phenomena around us.
An example of work:
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.
To solution
Let’s say we have a single-linked list (as described in the chapter Recursion and stack):
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
Make two variants of the solution: using a loop and using recursion.
To solution
Output a single-linked list from the previous task Output a single-linked list in the reverse order.
To solution
In this chapter we’ll learn how to do the same. And also, how to pass arrays to such functions as
parameters.
A function can be called with any number of arguments, no matter how it is defined.
Like here:
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 of the parameters can be included in the function definition by using three dots ...
followed by the name of the array that will contain them. The dots literally mean “gather the
remaining parameters into an array”.
return sum;
}
alert( sumAll(1) ); // 1
alert( sumAll(1, 2) ); // 3
alert( sumAll(1, 2, 3) ); // 6
We can choose to get the first parameters as variables, and gather only the rest.
Here the first two arguments go into variables and the rest go into titles array:
For instance:
function showName() {
alert( arguments.length );
alert( arguments[0] );
alert( arguments[1] );
// it's iterable
// for(let arg of arguments) alert(arg);
}
In old times, rest parameters did not exist in the language, and using arguments was the only
way to get all arguments of the function. And it still works, we can find it in the old code.
But the downside is that although arguments is both array-like and iterable, it’s not an array. It
does not support array methods, so we can’t call arguments.map(...) for example.
Also, it always contains all arguments. We can’t capture them partially, like we did with rest
parameters.
Here’s an example:
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
We’ve just seen how to get an array from the list of parameters.
alert( Math.max(3, 5, 1) ); // 5
Now let’s say we have an array [3, 5, 1] . How do we call Math.max with it?
Passing it “as is” won’t work, because Math.max expects a list of numeric arguments, not a
single array:
And surely we can’t manually list items in the code Math.max(arr[0], arr[1], arr[2]) ,
because we may be unsure how many there are. As our script executes, there could be a lot, or
there could be none. And that would get ugly.
Spread operator to the rescue! It looks similar to rest parameters, also using ... , but does quite
the opposite.
When ...arr is used in the function call, it “expands” an iterable object arr into the list of
arguments.
For Math.max :
In the examples above we used an array to demonstrate the spread operator, but any iterable will
do.
For instance, here we use the spread operator to turn the string into array of characters:
The spread operator internally uses iterators to gather elements, the same way as for..of
does.
For this particular task we could also use Array.from , because it converts an iterable (like a
string) into an array:
So, for the task of turning something into an array, Array.from tends to be more universal.
Summary
When we see "..." in the code, it is either rest parameters or the spread operator.
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.
Closure
JavaScript is a very function-oriented language. It gives us a lot of freedom. A function can be
created dynamically, copied to another variable or passed as an argument to another function
and called from a totally different place later.
We know that a function can access variables outside of it, this feature is used quite often.
But what happens when an outer variable changes? Does a function get the most recent value or
the one that existed when the function was created?
Also, what happens when a function travels to another place in the code and is called from there
– does it get access to the outer variables of the new place?
Different languages behave differently here, and in this chapter we cover the behaviour of
JavaScript.
A couple of questions
Let’s consider two situations to begin with, and then study the internal mechanics piece-by-piece,
so that you’ll be able to answer the following questions and more complex ones in the future.
1. The function sayHi uses an external variable name . When the function runs, which value is
it going to use?
function sayHi() {
alert("Hi, " + name);
}
name = "Pete";
Such situations are common both in browser and server-side development. A function may be
scheduled to execute later than it is created, for instance after a user action or a network
request.
2. The function makeWorker makes another function and returns it. That new function can be
called from somewhere else. Will it have access to the outer variables from its creation place,
or the invocation place, or both?
function makeWorker() {
let name = "Pete";
return function() {
alert(name);
};
}
// create a function
let work = makeWorker();
// call it
work(); // what will it show? "Pete" (name where created) or "John" (name where called)?
Lexical Environment
To understand what’s going on, let’s first discuss what a “variable” actually is.
In JavaScript, every running function, code block {...} , and the script as a whole have an
internal (hidden) associated object known as the Lexical Environment.
The Lexical Environment object consists of two parts:
1. Environment Record – an object that stores all local variables as its properties (and some
other information like the value of this ).
2. A reference to the outer lexical environment, the one associated with the outer code.
A “variable” is just a property of the special internal object, Environment Record . “To
get or change a variable” means “to get or change a property of that object”.
For instance, in this simple code, there is only one Lexical Environment:
LexicalEnvironment
outer
phrase: "Hello" null
This is a so-called global Lexical Environment, associated with the whole script.
On the picture above, the rectangle means Environment Record (variable store) and the arrow
means the outer reference. The global Lexical Environment has no outer reference, so it points to
null .
outer
execution start <empty> null
phrase: undefined
phrase: "Hello"
phrase: "Bye"
Rectangles on the right-hand side demonstrate how the global Lexical Environment changes
during the execution:
To summarize:
●
A variable is a property of a special internal object, associated with the currently executing
block/function/script.
●
Working with variables is actually working with the properties of that object.
Function Declaration
Until now, we only observed variables. Now enter Function Declarations.
Unlike let variables, they are fully initialized not when the execution reaches them, but
earlier, when a Lexical Environment is created.
For top-level functions, it means the moment when the script is started.
The code below demonstrates that the Lexical Environment is non-empty from the beginning. It
has say , because that’s a Function Declaration. And later it gets phrase , declared with let :
outer
execution start say: function null
say: function
phrase: "Hello"
During the call, say() uses the outer variable phrase . Let’s look at the details of what’s going
on.
When a function runs, a new Lexical Environment is created automatically to store local variables
and parameters of the call.
For instance, for say("John") , it looks like this (the execution is at the line, labelled with an
arrow):
LexicalEnvironment for the call
So, during the function call we have two Lexical Environments: the inner one (for the function call)
and the outer one (global):
●
The inner Lexical Environment corresponds to the current execution of say .
It has a single property: name , the function argument. We called say("John") , so the
value of name is "John" .
●
The outer Lexical Environment is the global Lexical Environment.
When the code wants to access a variable – the inner Lexical Environment is searched
first, then the outer one, then the more outer one and so on until the global one.
If a variable is not found anywhere, that’s an error in strict mode. Without use strict , an
assignment to a non-existing variable like user = "John" creates a new global variable
user . That’s for backwards compatibility.
Now we can give the answer to the first question from the beginning of the chapter.
A function gets outer variables as they are now, it uses the most recent values.
Old variable values are not saved anywhere. When a function wants a variable, it takes the
current value from its own Lexical Environment or the outer one.
function sayHi() {
alert("Hi, " + name);
}
sayHi(); // Pete
Nested functions
Here the nested function getFullName() is made for convenience. It can access the outer
variables and so can return the full name. Nested functions are quite common in JavaScript.
What’s much more interesting, a nested function can be returned: either as a property of a new
object (if the outer function creates an object with methods) or as a result by itself. It can then be
used somewhere else. No matter where, it still has access to the same outer variables.
For instance, here the nested function is assigned to the new object by the constructor function:
function makeCounter() {
let count = 0;
return function() {
return count++; // has access to the outer "count"
};
}
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
Let’s go on with the makeCounter example. It creates the “counter” function that returns the
next number on each invocation. Despite being simple, slightly modified variants of that code
have practical uses, for instance, as a pseudorandom number generator , and more.
When the inner function runs, the variable in count++ is searched from inside out. For the
example above, the order will be:
3
2
In this example count is found on step 2 . When an outer variable is modified, it’s changed
where it’s found. So count++ finds the outer variable and increases it in the Lexical
Environment where it belongs. Like if we had let count = 1 .
All done?
1. There is no way: count is a local function variable, we can’t access it from the outside.
2. For every call to makeCounter() a new function Lexical Environment is created, with its
own count . So the resulting counter functions are independent.
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
alert( counter1() ); // 0
alert( counter1() ); // 1
Hopefully, the situation with outer variables is clear now. For most situations such understanding
is enough. There are few details in the specification that we omitted for brevity. So in the next
section we cover even more details.
Environments in detail
Here’s what’s going on in the makeCounter example step-by-step. Follow it to make sure that
you understand how it works in detail.
Please note the additional [[Environment]] property is covered here. We didn’t mention it
before for simplicity.
1. When the script has just started, there is only the global Lexical Environment:
outer
[[Environment]] makeCounter: function null
At that starting moment there is only the makeCounter function, because it’s a Function
Declaration. It did not run yet.
All functions “on birth” receive a hidden property [[Environment]] with a reference
to the Lexical Environment of their creation.
We didn’t talk about it before. That’s how the function knows where it was made.
In other words, a function is “imprinted” with a reference to the Lexical Environment where it
was born. And [[Environment]] is the hidden function property that has that reference.
2. The code runs on, the new global variable counter is declared and gets the result of the
makeCounter() call. Here’s a snapshot of the moment when the execution is on the first
line inside makeCounter() :
outer outer
count: 0 makeCounter: function null
counter: undefined
At the moment of the call of makeCounter() , the Lexical Environment is created, to hold its
variables and arguments.
It doesn’t matter whether the function is created using Function Declaration or Function
Expression. All functions get the [[Environment]] property that references the Lexical
Environment in which they were made. So our new tiny nested function gets it as well.
For our new nested function the value of [[Environment]] is the current Lexical
Environment of makeCounter() (where it was born):
outer outer
[[Environment]] count: 0 makeCounter: function null
counter: undefined
Please note that on this step the inner function was created, but not yet called. The code inside
return count++; is not running.
4. As the execution goes on, the call to makeCounter() finishes, and the result (the tiny
nested function) is assigned to the global variable counter :
outer outer
[[Environment]] count: 0 makeCounter: function null
counter: function
That function has only one line: return count++ , that will be executed when we run it.
5. When counter() is called, a new Lexical Environment is created for the call. It’s empty, as
counter has no local variables by itself. But the [[Environment]] of counter is used
as the outer reference for it, that provides access to the variables of the former
makeCounter() call where it was created:
counter: function
[[Environment]]
Now when the call looks for count variable, it first searches its own Lexical Environment
(empty), then the Lexical Environment of the outer makeCounter() call, where it finds it.
Please note how memory management works here. Although makeCounter() call finished
some time ago, its Lexical Environment was retained in memory, because there’s a nested
function with [[Environment]] referencing it.
Generally, a Lexical Environment object lives as long as there is a function which may use it.
And only when there are none remaining, it is cleared.
6. The call to counter() not only returns the value of count , but also increases it. Note that
the modification is done “in place”. The value of count is modified exactly in the environment
where it was found.
modified here
outer outer
[[Environment]] count: 1 makeCounter: function null
counter: function
The answer to the second question from the beginning of the chapter should now be obvious.
The work() function in the code below gets name from the place of its origin through the outer
lexical environment reference:
But if there were no let name in makeWorker() , then the search would go outside and take
the global variable as we can see from the chain above. In that case it would be "John" .
Closures
There is a general programming term “closure”, that developers generally should know.
A closure is a function that remembers its outer variables and can access them. In some
languages, that’s not possible, or a function should be written in a special way to make it
happen. But as explained above, in JavaScript, all functions are naturally closures (there is
only one exclusion, to be covered in The "new Function" syntax).
That is: they automatically remember where they were created using a hidden
[[Environment]] property, and all of them can access outer variables.
When on an interview, a frontend developer gets a question about “what’s a closure?”, a valid
answer would be a definition of the closure and an explanation that all functions in JavaScript
are closures, and maybe a few more words about technical details: the [[Environment]]
property and how Lexical Environments work.
Code blocks and loops, IIFE
The examples above concentrated on functions. But a Lexical Environment exists for any code
block {...} .
A Lexical Environment is created when a code block runs and contains block-local variables.
Here are a couple of examples.
If
In the example below, the user variable exists only in the if block:
outer outer
user: "John" phrase: "Hello" null
When the execution gets into the if block, the new “if-only” Lexical Environment is created for
it.
It has the reference to the outer one, so phrase can be found. But all variables and Function
Expressions, declared inside if , reside in that Lexical Environment and can’t be seen from the
outside.
For instance, after if finishes, the alert below won’t see the user , hence the error.
For, while
For a loop, every iteration has a separate Lexical Environment. If a variable is declared in
for(let ...) , then it’s also in there:
Please note: let i is visually outside of {...} . The for construct is special here: each
iteration of the loop has its own Lexical Environment with the current i in it.
Code blocks
We also can use a “bare” code block {…} to isolate variables into a “local scope”.
For instance, in a web browser all scripts (except with type="module" ) share the same global
area. So if we create a global variable in one script, it becomes available to others. But that
becomes a source of conflicts if two scripts use the same variable name and overwrite each
other.
That may happen if the variable name is a widespread word, and script authors are unaware of
each other.
If we’d like to avoid that, we can use a code block to isolate the whole script or a part of it:
{
// do some job with local variables that should not be seen outside
alert(message); // Hello
}
The code outside of the block (or inside another script) doesn’t see variables inside the block,
because the block has its own Lexical Environment.
IIFE
In the past, there were no block-level lexical environments in JavaScript.
So programmers had to invent something. And what they did was called “immediately-invoked
function expressions” (abbreviated as IIFE).
That’s not a thing we should use nowadays, but you can find them in old scripts, so it’s better to
understand them.
(function() {
alert(message); // Hello
})();
Here a Function Expression is created and immediately called. So the code executes right away
and has its own private variables.
The Function Expression is wrapped with parenthesis (function {...}) , because when
JavaScript meets "function" in the main code flow, it understands it as the start of a Function
Declaration. But a Function Declaration must have a name, so this kind of code will give an error:
alert(message); // Hello
}();
Even if we say: “okay, let’s add a name”, that won’t work, as JavaScript does not allow Function
Declarations to be called immediately:
// syntax error because of parentheses below
function go() {
So, the parentheses around the function is a trick to show JavaScript that the function is created
in the context of another expression, and hence it’s a Function Expression: it needs no name and
can be called immediately.
There exist other ways besides parentheses to tell JavaScript that we mean a Function
Expression:
(function() {
alert("Parentheses around the function");
})();
(function() {
alert("Parentheses around the whole thing");
}());
!function() {
alert("Bitwise NOT operator starts the expression");
}();
+function() {
alert("Unary plus starts the expression");
}();
In all the above cases we declare a Function Expression and run it immediately. Let’s note again:
nowadays there’s no reason to write such code.
Garbage collection
Usually, a Lexical Environment is cleaned up and deleted after the function runs. For instance:
function f() {
let value1 = 123;
let value2 = 456;
}
f();
Here, two values are technically the properties of the Lexical Environment. But after f()
finishes, that Lexical Environment becomes unreachable, so it’s deleted from the memory.
…But if there’s a nested function that is still reachable after the end of f , then it has
[[Environment]] property that references the outer lexical environment, so it’s also
reachable and alive:
function f() {
let value = 123;
return g;
}
Please note that if f() is called many times, and resulting functions are saved, then all
corresponding Lexical Environment objects will also be retained in memory. All 3 of them in the
code below:
function f() {
let value = Math.random();
A Lexical Environment object dies when it becomes unreachable (just like any other object). In
other words, it exists only while there’s at least one nested function referencing it.
In the code below, after g becomes unreachable, its enclosing Lexical Environment (and hence
the value ) is cleaned from memory;
function f() {
let value = 123;
return g;
}
Real-life optimizations
As we’ve seen, in theory while a function is alive, all outer variables are also retained.
But in practice, JavaScript engines try to optimize that. They analyze variable usage and if it’s
obvious from the code that an outer variable is not used – it is removed.
An important side effect in V8 (Chrome, Opera) is that such variable will become
unavailable in debugging.
Try running the example below in Chrome with the Developer Tools open.
When it pauses, in the console type alert(value) .
function f() {
let value = Math.random();
function g() {
debugger; // in console: type alert(value); No such variable!
}
return g;
}
let g = f();
g();
As you could see – there is no such variable! In theory, it should be accessible, but the engine
optimized it out.
That may lead to funny (if not such time-consuming) debugging issues. One of them – we can
see a same-named outer variable instead of the expected one:
function f() {
let value = "the closest value";
function g() {
debugger; // in console: type alert(value); Surprise!
}
return g;
}
let g = f();
g();
⚠ See ya!
This feature of V8 is good to know. If you are debugging with Chrome/Opera, sooner or later
you will meet it.
That is not a bug in the debugger, but rather a special feature of V8. Perhaps it will be
changed sometime. You always can check for it by running the examples on this page.
✔ Tasks
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?
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter2() ); // ?
alert( counter2() ); // ?
To solution
Counter object
importance: 5
Here a counter object is made with the help of the constructor function.
function Counter() {
let count = 0;
this.up = function() {
return ++count;
};
this.down = function() {
return --count;
};
}
alert( counter.up() ); // ?
alert( counter.up() ); // ?
alert( counter.down() ); // ?
To solution
Function in if
Look at the code. What will be the result of the call at the last line?
let phrase = "Hello";
if (true) {
let user = "John";
function sayHi() {
alert(`${phrase}, ${user}`);
}
}
sayHi();
To solution
For instance:
sum(1)(2) = 3
sum(5)(-1) = 4
To solution
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.
For instance:
To solution
Sort by field
importance: 5
let users = [
{ name: "John", age: 20, surname: "Johnson" },
{ name: "Pete", age: 18, surname: "Peterson" },
{ name: "Ann", age: 19, surname: "Hathaway" }
];
users.sort(byField('name'));
users.sort(byField('age'));
To solution
Army of functions
importance: 5
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;
}
Why do all of the shooters show the same value? Fix the code so that they work as intended.
To solution
let and const behave exactly the same way in terms of Lexical Environments.
But var is a very different beast, that originates from very old times. It’s generally not used in
modern scripts, but still lurks in the old ones.
If you don’t plan on meeting such scripts you may even skip this chapter or postpone it, but then
there’s a chance that it bites you later.
From the first sight, var behaves similar to let . That is, declares a variable:
function sayHi() {
var phrase = "Hello"; // local variable, "var" instead of "let"
alert(phrase); // Hello
}
sayHi();
For instance:
if (true) {
var test = true; // use "var" instead of "let"
}
If we used let test instead of var test , then the variable would only be visible inside if :
if (true) {
let test = true; // use "let"
}
function sayHi() {
if (true) {
var phrase = "Hello";
}
alert(phrase); // works
}
sayHi();
alert(phrase); // Error: phrase is not defined (Check the Developer Console)
As we can see, var pierces through if , for or other code blocks. That’s because a long time
ago in JavaScript blocks had no Lexical Environments. And var is a remnant of that.
var declarations are processed when the function starts (or script starts for globals).
In other words, var variables are defined from the beginning of the function, no matter where
the definition is (assuming that the definition is not in the nested function).
So this code:
function sayHi() {
phrase = "Hello";
alert(phrase);
var phrase;
}
sayHi();
function sayHi() {
var phrase;
phrase = "Hello";
alert(phrase);
}
sayHi();
function sayHi() {
phrase = "Hello"; // (*)
if (false) {
var phrase;
}
alert(phrase);
}
sayHi();
People also call such behavior “hoisting” (raising), because all var are “hoisted” (raised) to the
top of the function.
So in the example above, if (false) branch never executes, but that doesn’t matter. The
var inside it is processed in the beginning of the function, so at the moment of (*) the variable
exists.
function sayHi() {
alert(phrase);
The declaration is processed at the start of function execution (“hoisted”), but the assignment
always works at the place where it appears. So the code works essentially like this:
function sayHi() {
var phrase; // declaration works at the start...
alert(phrase); // undefined
sayHi();
Because all var declarations are processed at the function start, we can reference them at any
place. But variables are undefined until the assignments.
In both examples above alert runs without an error, because the variable phrase exists. But
its value is not yet assigned, so it shows undefined .
Summary
1. var variables have no block scope, they are visible minimum at the function level.
2. var declarations are processed at function start (script start for globals).
There’s one more minor difference related to the global object, we’ll cover that in the next
chapter.
These differences make var worse than let most of the time. Block-level variables is such a
great thing. That’s why let was introduced in the standard long ago, and is now a major way
(along with const ) to declare a variable.
Global object
The global object provides variables and functions that are available anywhere. By default, those
that are built into the language or the environment.
In a browser it is named window , for Node.js it is global , for other environments it may have
another name.
Recently, globalThis was added to the language, as a standardized name for a global object,
that should be supported across all environments. In some browsers, namely non-Chromium
Edge, globalThis is not yet supported, but can be easily polyfilled.
We’ll use window here, assuming that our environment is a browser. If your script may run in
other environments, it’s better to use globalThis instead.
alert("Hello");
// is the same as
window.alert("Hello");
In a browser, global functions and variables declared with var (not let/const !) become the
property of the global object:
var gVar = 5;
Please don’t rely on that! This behavior exists for compatibility reasons. Modern scripts use
JavaScript modules where such thing doesn’t happen.
let gLet = 5;
If a value is so important that you’d like to make it available globally, write it directly as a property:
That said, using global variables is generally discouraged. There should be as few global
variables as possible. The code design where a function gets “input” variables and produces
certain “outcome” is clearer, less prone to errors and easier to test than if it uses outer or global
variables.
We use the global object to test for support of modern language features.
For instance, test if a built-in Promise object exists (it doesn’t in really old browsers):
if (!window.Promise) {
alert("Your browser is really old!");
}
If there’s none (say, we’re in an old browser), we can create “polyfills”: add functions that are not
supported by the environment, but exist in the modern standard.
if (!window.Promise) {
window.Promise = ... // custom implementation of the modern language feature
}
Summary
●
The global object holds variables that should be available everywhere.
That includes JavaScript built-ins, such as Array and environment-specific values, such as
window.innerHeight – the window height in the browser.
●
The global object has a universal name globalThis .
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.
function sayHi() {
alert("Hi");
}
alert(sayHi.name); // sayHi
What’s kind of funny, the name-assigning logic is smart. It also assigns the correct name to a
function even if it’s created without one, and then immediately assigned:
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.
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. In
that case, the name property is empty, like here:
There is another built-in property “length” that returns the number of function parameters, for
instance:
function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}
alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2
The length property is sometimes used for introspection in functions that operate on other
functions.
For instance, in the code below the ask function accepts a question to ask and an arbitrary
number of handler functions to call.
Once a user provides their answer, the function calls the handlers. We can pass two kinds of
handlers:
●
A zero-argument function, which is only called when the user gives a positive answer.
● A function with arguments, which is called in either case and returns an answer.
The idea is that we have a simple, no-arguments handler syntax for positive cases (most frequent
variant), but are able to support universal handlers as well:
Here we add the counter property to track the total calls count:
function sayHi() {
alert("Hi");
sayHi(); // Hi
sayHi(); // Hi
We can treat a function as an object, store properties in it, but that has no effect on its
execution. Variables are not function properties and vice versa. These are just parallel worlds.
Function properties can replace closures sometimes. For instance, we can rewrite the counter
function example from the chapter Closure to use a function property:
function makeCounter() {
// instead of:
// let count = 0
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
The count is now stored in the function directly, not in its outer Lexical Environment.
The main difference is that if the value of count lives in an outer variable, then external code is
unable to access it. Only nested functions may modify it. And if it’s bound to a function, then such
a thing is possible:
function makeCounter() {
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
counter.count = 10;
alert( counter() ); // 10
Named Function Expression, or NFE, is a term for Function Expressions that have a name.
Did we achieve anything here? What’s the purpose 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.
For instance, the function sayHi below calls itself again with "Guest" if no who is provided:
Why do we use func ? Maybe just use sayHi for the nested call?
The problem with that code is that sayHi may change in the outer code. If the function gets
assigned to another variable instead, the code will start to give errors:
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 meant to solve exactly these
kinds of problems.
Now it works, because the name "func" is function-local. It is not taken from outside (and not
visible there). The specification guarantees that it will always reference the current function.
The outer code still has it’s variable sayHi or welcome . And func is an “internal function
name”, how the function can call itself internally.
Sometimes, when we need a reliable internal name, it’s the reason to rewrite a Function
Declaration to Named Function Expression form.
Summary
If the function is declared as a Function Expression (not in the main code flow), and it carries the
name, then it is called a Named Function Expression. The name can be used inside to reference
itself, for recursive calls or such.
Also, functions may carry additional properties. Many well-known JavaScript libraries make great
use of this feature.
They create a “main” function and attach many other “helper” functions to it. For instance, the
jQuery library creates a function named $ . The lodash library creates a function _ , and
then adds _.clone , _.keyBy and other properties to it (see the docs when you want learn
more about them). Actually, they do it to lessen their pollution of the global space, so that a single
library gives only one global variable. That reduces the possibility of naming conflicts.
So, a function can do a useful job by itself and also carry a bunch of other functionality in
properties.
✔ Tasks
Modify the code of makeCounter() so that the counter can also decrease and set the number:
P.S. You can use either a closure or the function property to keep the current count. Or write both
variants.
To solution
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.
To solution
Syntax
The function is created with the arguments arg1...argN and the given functionBody .
It’s easier to understand by looking at an example. Here’s a function with two arguments:
alert( sum(1, 2) ); // 3
And here there’s a function without arguments, with only the function body:
sayHi(); // Hello
The major difference from other ways we’ve seen is that the function is created literally from a
string, that is passed at run time.
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 a server and then execute it:
let str = ... receive the code from a server dynamically ...
It is used in very specific cases, like when we receive code from a server, or to dynamically
compile a function from a template, in complex web-applications.
Closure
Usually, a function remembers where it was born in the special property [[Environment]] . It
references the Lexical Environment from where it’s created (we covered that in the chapter
Closure).
But when a function is created using new Function , its [[Environment]] is set to
reference not the current Lexical Environment, but the global one.
So, such function doesn’t have access to outer variables, only to the global ones.
function getFunc() {
let value = "test";
return func;
}
function getFunc() {
let value = "test";
return func;
}
This special feature of new Function looks strange, but appears very useful in practice.
Imagine that we must create a function from a 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.
The problem is that before JavaScript is published to production, it’s compressed using a minifier
– a special program that shrinks code by removing extra comments, spaces and – what’s
important, renames local variables into shorter ones.
For instance, if a function has let userName , minifier replaces it let a (or another letter if
this one is occupied), and does it everywhere. That’s usually a safe thing to do, because the
variable is local, nothing outside the function can access it. And inside the function, minifier
replaces every mention of it. Minifiers are smart, they analyze the code structure, so they don’t
break anything. They’re not just a dumb find-and-replace.
So if new Function had access to outer variables, it would be unable to find renamed
userName .
If new Function had access to outer variables, it would have problems with minifiers.
To pass something to a function, created as new Function , we should use its arguments.
Summary
The syntax:
Functions created with new Function , have [[Environment]] referencing the global
Lexical Environment, not the outer one. Hence, they cannot use outer variables. But that’s
actually good, because it insures us from errors. Passing parameters explicitly is a much better
method architecturally and causes no problems with minifiers.
These methods are not a part of JavaScript specification. But most environments have the
internal scheduler and provide these methods. In particular, they are supported in all browsers
and Node.js.
setTimeout
The syntax:
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), by default 0.
arg1 , arg2 …
Arguments for the function (not supported in IE9-)
function sayHi() {
alert('Hello');
}
setTimeout(sayHi, 1000);
With arguments:
If the first argument is a string, then JavaScript creates a function from it.
setTimeout("alert('Hello')", 1000);
But using strings is not recommended, use arrow functions instead of them, like this:
// wrong!
setTimeout(sayHi(), 1000);
That doesn’t work, because setTimeout expects a reference to a function. And here
sayHi() runs the function, and the result of its execution is passed to setTimeout . In
our case the result of sayHi() is undefined (the function returns nothing), so nothing is
scheduled.
In the code below, we schedule the function and then cancel it (changed our mind). As a result,
nothing happens:
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, this can be something else. For instance, Node.js returns a timer object with
additional methods.
For browsers, timers are described in the timers section of HTML5 standard.
setInterval
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:
So if you run the code above and don’t dismiss the alert window for some time, then in the
next alert will be shown immediately as you do it. The actual interval between alerts will be
shorter than 2 seconds.
Nested setTimeout
The setTimeout above schedules the next call right at the end of the current one (*) .
The nested setTimeout is a more flexible method than setInterval . This way the next call
may be scheduled differently, depending on the results of the current one.
For instance, we need to write a service that sends a request to the server every 5 seconds
asking for data, but in case the server is overloaded, it should increase the interval to 10, 20, 40
seconds…
}, delay);
And if the functions that we’re scheduling are CPU-hungry, then we can measure the time taken
by the execution and plan the next call sooner or later.
Nested setTimeout allows to set the delay between the executions more precisely than
setInterval .
Let’s compare two code fragments. The first one uses setInterval :
let i = 1;
setInterval(function() {
func(i++);
}, 100);
let i = 1;
setTimeout(function run() {
func(i++);
setTimeout(run, 100);
}, 100);
For setInterval the internal scheduler will run func(i++) every 100ms:
The real delay between func calls for setInterval is less than in the code!
That’s normal, because the time taken by func 's execution “consumes” a part of the interval.
It is possible that func 's 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, runs it again immediately.
In the edge case, if the function always executes longer than delay ms, then the calls will
happen without a pause at all.
100 100
That’s because a new call is planned at the end of the previous one.
Garbage collection and setInterval/setTimeout callback
When a function is passed in setInterval/setTimeout , an internal reference is created
to it and saved in the scheduler. It prevents the function from being garbage collected, even if
there are no other references to it.
There’s a side-effect. A function references the outer lexical environment, so, while it lives,
outer variables live too. They may take much more memory than the function itself. So when
we don’t need the scheduled function anymore, it’s better to cancel it, even if it’s very small.
This schedules the execution of func as soon as possible. But the scheduler will invoke it only
after the currently executing script is complete.
alert("Hello");
The first line “puts the call into calendar after 0ms”. But the scheduler will only “check the
calendar” after the current script is complete, so "Hello" is first, and "World" – after it.
There are also advanced browser-related use cases of zero-delay timeout, that we’ll discuss in
the chapter Event loop: microtasks and macrotasks.
Zero delay is in fact not zero (in a browser)
In the browser, there’s a limitation of how often nested timers can run. The HTML5
standard says: “after five nested timers, the interval is forced to be at least 4
milliseconds.”.
Let’s demonstrate what it means with the example below. The setTimeout call in it re-
schedules itself with zero delay. Each call remembers the real time from the previous one in
the times array. What do the real delays look like? Let’s see:
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); // else re-schedule
});
First timers run immediately (just as written in the spec), and then we see 9, 15, 20,
24... . The 4+ ms obligatory delay between invocations comes into play.
That limitation comes from ancient times and many scripts rely on it, so it exists for historical
reasons.
For server-side JavaScript, that limitation does not exist, and there exist other ways to
schedule an immediate asynchronous job, like setImmediate for Node.js. So this note is
browser-specific.
Summary
●
Methods setTimeout(func, delay, ...args) and setInterval(func, delay,
...args) allow us to run the func once/regularly after delay milliseconds.
●
To cancel the execution, we should call clearTimeout/clearInterval with the value
returned by setTimeout/setInterval .
●
Nested setTimeout calls are a more flexible alternative to setInterval , allowing us to
set the time between executions more precisely.
●
Zero delay scheduling with setTimeout(func, 0) (the same as setTimeout(func) )
is used to schedule the call “as soon as possible, but after the current script is complete”.
●
The browser limits the minimal delay for five or more nested call of setTimeout or for
setInterval (after 5th call) to 4ms. That’s for historical reasons.
Please note that all scheduling methods do not guarantee the exact delay.
For example, the in-browser timer may slow down for a lot of reasons:
●
The CPU is overloaded.
●
The browser tab is in the background mode.
● The laptop is on battery.
All that may increase the minimal timer resolution (the minimal delay) to 300ms or even 1000ms
depending on the browser and OS-level performance settings.
✔ Tasks
Write a function printNumbers(from, to) that outputs a number every second, starting
from from and ending with to .
1. Using setInterval .
2. Using nested setTimeout .
To solution
In the code below there’s a setTimeout call scheduled, then a heavy calculation is run, that
takes more than 100ms to finish.
let i = 0;
To solution
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.
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 to avoid spending
extra-time on recalculations.
But instead of adding that functionality into slow() we’ll create a wrapper function, that adds
caching. As we’ll see, there are many benefits of doing so.
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 there's such key in cache
return cache.get(x); // read the result from it
}
slow = cachingDecorator(slow);
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.
The result of cachingDecorator(func) is a “wrapper”: function(x) that “wraps” the call
of func(x) into caching logic:
wrapper
From an outside code, the wrapped slow function still does the same. It just got a caching
aspect added to its behavior.
The caching decorator mentioned above is not suited to work with object methods.
For instance, in the code below worker.slow() stops working after the decoration:
slow(x) {
// scary CPU-heavy task here
alert("Called with " + x);
return x * this.someMethod(); // (*)
}
};
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 .
So, the wrapper passes the call to the original method, but without the context this . Hence the
error.
It runs func providing the first argument as this , and the next as the arguments.
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 :
function sayHi() {
alert(this.name);
}
And here we use call to call say with the given context and phrase:
function say(phrase) {
alert(this.name + ': ' + phrase);
}
In our case, we can use call in the wrapper to pass the context to the original function:
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;
};
}
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.
Now let’s make cachingDecorator even more universal. Till now it was working only with
single-argument functions.
let worker = {
slow(min, max) {
return min + max; // scary CPU-hogger is assumed
}
};
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.
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 one value from many.
For many practical applications, the 3rd variant is good enough, so we’ll stick to it.
let worker = {
slow(min, max) {
alert(`Called with ${min},${max}`);
return min + max;
}
};
cache.set(key, result);
return result;
};
}
function hash(args) {
return args[0] + ',' + args[1];
}
Now it works with any number of arguments (though the hash function would also need to be
adjusted to allow any number of arguments. An interesting way to handle this will be covered
below).
There are two changes:
●
In the line (*) it calls hash to create a single key from arguments . Here we use a simple
“joining” function that turns arguments (3, 5) into the key "3,5" . More complex cases
may require other hashing functions.
●
Then (**) uses func.call(this, ...arguments) to pass both the context and all
arguments the wrapper got (not just the first one) to the original function.
func.apply(context, args)
It runs the func setting this=context and using an array-like object args as the list of
arguments.
The only syntax difference between call and apply is that call expects a list of
arguments, while apply takes an array-like object with them.
And for objects that are both iterable and array-like, like a real array, we technically could use any
of them, but apply will probably be faster, because most JavaScript engines internally optimize
it better.
Passing all arguments along with the context to another function is called call forwarding.
When an external code calls such wrapper , it is indistinguishable from the call of the original
function func .
Borrowing a method
Now let’s make one more minor improvement in the hashing function:
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 .
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.
function hash() {
alert( arguments.join() ); // Error: arguments.join is not a function
}
hash(1, 2);
hash(1, 2);
We take (borrow) a join method from a regular array ( [].join ) and use [].join.call to
run it in the context of arguments .
That’s because the internal algorithm of the native method arr.join(glue) is very simple.
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 .
It is generally safe to replace a function or a method with a decorated one, except for one little
thing. If the original function had properties on it, like func.calledCount or whatever, then
the decorated one will not provide them. Because that is a wrapper. So one needs to be careful if
one uses them.
E.g. in the example above if slow function had any properties on it, then
cachingDecorator(slow) is a wrapper without them.
Some decorators may provide their own properties. E.g. a decorator may count how many times
a function was invoked and how much time it took, and expose this information via wrapper
properties.
There exists a way to create decorators that keep access to function properties, but this requires
using a special Proxy object to wrap a function. We’ll discuss it later in the article Proxy and
Reflect.
Summary
Decorator is a wrapper around a function that alters its behavior. The main job is still carried out
by the function.
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!
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.
✔ Tasks
Spy decorator
importance: 5
Create a decorator spy(func) that should return a wrapper that saves all calls to function in its
calls property.
For instance:
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
P.S. That decorator is sometimes useful for unit-testing. Its advanced form is sinon.spy in
Sinon.JS library.
Delaying decorator
importance: 5
For instance:
function f(x) {
alert(x);
}
// create wrappers
let f1000 = delay(f, 1000);
let f1500 = delay(f, 1500);
In the code above, f is a function of a single argument, but your solution should pass all
arguments and the context this .
To solution
Debounce decorator
importance: 5
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 future calls to the
function made less than ms milliseconds after the previous call will be ignored.
For instance:
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.
Open a sandbox with tests.
To solution
Throttle decorator
importance: 5
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.
In a browser we can setup a function to run at every mouse movement and get the pointer
location as it moves. During an active mouse usage, this function usually runs very frequently,
can be something like 100 times per second (every 10 ms).
We’d like to update some information on the web-page when the pointer moves.
…But updating function update() is too heavy to do it on every micro-movement. There is also
no sense in updating more often than once per 100ms.
So we’ll wrap it into the decorator: use throttle(update, 100) as the function to run on
each mouse move instead of the original update() . The decorator will be called often, but
forward the call to update() at maximum once per 100ms.
1. For the first mouse movement the decorated variant immediately passes the call to
update . That’s important, the user sees our reaction to their move immediately.
2. Then as the mouse moves on, until 100ms nothing happens. The decorated variant ignores
calls.
3. At the end of 100ms – one more update happens with the last coordinates.
4. Then, finally, the mouse stops somewhere. The decorated variant waits until 100ms expire
and then runs update with last coordinates. So, quite important, the final mouse coordinates
are processed.
A code example:
function f(a) {
console.log(a);
}
P.S. Arguments and the context this passed to f1000 should be passed to the original f .
To solution
Function binding
When passing object methods as callbacks, for instance to setTimeout , there’s a known
problem: "losing this ".
Losing “this”
We’ve already seen examples of losing this . Once a method is passed somewhere separately
from the object – this is lost.
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
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:
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, 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
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.
What if before setTimeout triggers (there’s one second delay!) user changes value? Then,
suddenly, it will call the wrong object!
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
Solution 2: bind
let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
All arguments are passed to the original func “as is”, for instance:
let user = {
firstName: "John"
};
function func(phrase) {
alert(phrase + ', ' + this.firstName);
}
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
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 :
let user = {
firstName: "John",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
JavaScript libraries also provide functions for convenient mass binding , e.g. _.bindAll(obj)
in lodash.
Partial functions
Until now we have only been talking about binding this . Let’s take it a step further.
We can bind not only this , but also arguments. That’s rarely done, but sometimes can be
handy.
function mul(a, b) {
return a * b;
}
function mul(a, b) {
return a * b;
}
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 – 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 .
function mul(a, b) {
return a * b;
}
The benefit is that we can create an independent function with a readable name ( double ,
triple ). We can use it and not provide the first argument every time as it’s fixed with bind .
In other cases, partial application is useful when we have a very generic function and want a less
universal variant of it for convenience.
For instance, we have a function send(from, to, text) . Then, inside a user object we
may want to use a partial variant of it: sendTo(to, text) that sends from the current user.
Going partial without context
What if we’d like to fix some arguments, but not the context this ? For example, for an object
method.
The native bind does not allow that. We can’t just omit the context and jump to arguments.
Fortunately, a function partial for binding only arguments can be easily implemented.
Like this:
// Usage:
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
user.sayNow("Hello");
// Something like:
// [10:00] John: Hello!
The result of partial(func[, arg1, arg2...]) call is a wrapper (*) that calls func
with:
●
Same this as it gets (for user.sayNow call it’s user )
●
Then gives it ...argsBound – arguments from the partial call ( "10:00" )
●
Then gives it ...args – arguments given to the wrapper ( "Hello" )
Summary
Usually we apply bind to fix this for an object method, so that we can pass it somewhere.
For example, to setTimeout .
When we fix some arguments of an existing function, the resulting (less universal) function is
called partially applied or partial.
Partials are convenient when we don’t want to repeat the same argument over and over again.
Like if we have a send(from, to) function, and from should always be the same for our
task, we can get a partial and go on with it.
✔ Tasks
function f() {
alert( this ); // ?
}
let user = {
g: f.bind(null)
};
user.g();
To solution
Second bind
importance: 5
function f() {
alert(this.name);
}
f();
To solution
There’s a value in the property of a function. Will it change after bind ? Why, or why not?
function sayHi() {
alert( this.name );
}
sayHi.test = 5;
let bound = sayHi.bind({
name: "John"
});
To solution
The call to askPassword() in the code below should check the password and then call
user.loginOk/loginFail depending on the answer.
Fix the highlighted line for everything to start working right (other lines are not to be changed).
let user = {
name: 'John',
loginOk() {
alert(`${this.name} logged in`);
},
loginFail() {
alert(`${this.name} failed to log in`);
},
};
askPassword(user.loginOk, user.loginFail);
To solution
The task is a little more complex variant of Fix a function that loses "this".
The user object was modified. Now instead of two functions loginOk/loginFail , it has a
single function user.login(true/false) .
What should we pass askPassword in the code below, so that it calls user.login(true)
as ok and user.login(false) as fail ?
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(?, ?); // ?
To solution
Arrow functions are not just a “shorthand” for writing small stuff. They have some very specific
and useful features.
JavaScript is full of situations where we need to write a small function that’s executed somewhere
else.
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. That’s where arrow
functions come in handy.
As we remember from the chapter Object methods, "this", arrow functions do not have this . If
this is accessed, it is taken from the outside.
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 .
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 .
For instance, defer(f, ms) gets a function and returns a wrapper around it that delays the
call by ms milliseconds:
function sayHi(who) {
alert('Hello, ' + who);
}
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 yet. We will on the chapter Class
inheritance
That’s because they are meant for short pieces of code that do not have their own “context”, but
rather work in the current one. And they really shine in that use case.
Until now, a property was a simple “key-value” pair to us. But an object property is actually a
more flexible and powerful thing.
In this chapter we’ll study additional configuration options, and in the next we’ll see how to
invisibly turn them into getter/setter functions.
Property flags
Object properties, besides a value , have three special attributes (so-called “flags”):
●
writable – if true , the value can be changed, otherwise it’s read-only.
●
enumerable – if true , then listed in loops, otherwise not listed.
●
configurable – if true , the property can be deleted and these attributes can be modified,
otherwise not.
We didn’t see them yet, because generally they do not show up. When we create a property “the
usual way”, all of them are true . But we also can change them anytime.
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:
let user = {
name: "John"
};
obj , propertyName
The object and its property to apply the descriptor.
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:
Object.defineProperty(user, "name", {
value: "John"
});
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 .
Non-writable
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
writable: false
});
Now no one can change the name of our user, unless they apply their own defineProperty
to override ours.
Here’s the same example, but the property is created from scratch:
let user = { };
Object.defineProperty(user, "name", {
value: "John",
// for new properties we need to explicitly list what's true
enumerable: true,
configurable: true
});
alert(user.name); // John
user.name = "Pete"; // Error
Non-enumerable
Normally, a built-in toString for objects is non-enumerable, it does not show up in for..in .
But if we add a toString of our own, then by default it shows up in for..in , like this:
let user = {
name: "John",
toString() {
return this.name;
}
};
If we don’t like it, then we can set enumerable:false . Then it won’t appear in a for..in
loop, just like the built-in one:
let user = {
name: "John",
toString() {
return this.name;
}
};
Object.defineProperty(user, "toString", {
enumerable: false
});
alert(Object.keys(user)); // name
Non-configurable
The non-configurable flag ( configurable:false ) is sometimes preset for built-in objects and
properties.
Math.PI = 3; // Error
let user = { };
Object.defineProperty(user, "name", {
value: "John",
writable: false,
configurable: false
});
The idea of configurable: false is to prevent changes to property flags and its
deletion, not changes to its value.
Object.defineProperties
Object.defineProperties(obj, {
prop1: descriptor1,
prop2: descriptor2
// ...
});
For instance:
Object.defineProperties(user, {
name: { value: "John", writable: false },
surname: { value: "Smith", writable: false },
// ...
});
Object.getOwnPropertyDescriptors
To get all property descriptors at once, we can use the method
Object.getOwnPropertyDescriptors(obj) .
Normally when we clone an object, we use an assignment to copy properties, like this:
…But that does not copy flags. So if we want a “better” clone then
Object.defineProperties is preferred.
There are also methods that limit access to the whole object:
Object.preventExtensions(obj)
Object.seal(obj)
Forbids adding/removing of properties. Sets configurable: false for all existing properties.
Object.freeze(obj)
Object.isExtensible(obj)
Object.isSealed(obj)
Returns true if adding/removing properties is forbidden, and all existing properties have
configurable: false .
Object.isFrozen(obj)
Returns true if adding/removing/changing properties is forbidden, and all current properties are
configurable: false, writable: false .
The first kind is data properties. We already know how to work with them. All properties that
we’ve been using until now were data properties.
The second type of properties is something new. It’s accessor properties. They are essentially
functions that work on getting and setting a value, but look like regular properties to an external
code.
Accessor properties are represented by “getter” and “setter” methods. In an object literal they are
denoted by get and set :
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.
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:
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 user = {
get fullName() {
return `...`;
}
};
let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${this.name} ${this.surname}`;
},
set fullName(value) {
[this.name, this.surname] = value.split(" ");
}
};
alert(user.name); // Alice
alert(user.surname); // Cooper
Accessor descriptors
Descriptors for accessor properties are different from those for data properties.
For accessor properties, there is no value or writable , but instead there are get and set
functions.
let user = {
name: "John",
surname: "Smith"
};
Object.defineProperty(user, 'fullName', {
get() {
return `${this.name} ${this.surname}`;
},
set(value) {
[this.name, this.surname] = value.split(" ");
}
});
Please note once again that a property can be either an accessor (has get/set methods) or a
data property (has a value ), not both.
If we try to supply both get and value in the same descriptor, there will be an error:
value: 2
});
Smarter getters/setters
Getters/setters can be used as wrappers over “real” property values to gain more control over
operations with them.
For instance, if we want to forbid too short names for user , we can have a setter name and
keep the value in a separate property _name :
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
So, the name is stored in _name property, and the access is done via getter and setter.
Technically, external code is able to access the name directly by using user._name . But there
is a widely known convention that properties starting with an underscore "_" are internal and
should not be touched from outside the object.
One of the great uses of accessors is that they allow to take control over a “regular” data property
at any moment by replacing it with a getter and a setter and tweak its behavior.
Imagine we started implementing user objects using data properties name and age :
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:
Now what to do with the old code that still uses age property?
We can try to find all such places and fix them, but that takes time and can be hard to do if that
code is used by many other people. And besides, age is a nice thing to have in user , right?
Now the old code works too and we’ve got a nice additional property.
Prototypes, inheritance
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.
[[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]]
object
The prototype is a little bit “magical”. When we want to read a property from object , and it’s
missing, JavaScript automatically takes it from the prototype. In programming, such thing is called
“prototypal inheritance”. Many cool language features and programming techniques are based on
it.
The property [[Prototype]] is internal and hidden, but there are many ways to set it.
rabbit.__proto__ = animal;
By the specification, __proto__ must only be supported by browsers, but in fact all
environments including server-side support it. For now, as __proto__ notation is a little bit
more intuitively obvious, we’ll use it in the examples.
If we look for a property in rabbit , and it’s missing, JavaScript automatically takes it from
animal .
For instance:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
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):
animal
eats: true
[[Prototype]]
rabbit
jumps: true
Here we can say that " animal is the prototype of rabbit " or " rabbit prototypically inherits
from animal ".
So if animal has a lot of useful properties and methods, then they become automatically
available in rabbit . Such properties are called “inherited”.
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
animal
eats: true
walk: function
[[Prototype]]
rabbit
jumps: true
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
[[Prototype]]
rabbit
jumps: true
[[Prototype]]
longEar
earLength: 10
1. The references can’t go in circles. JavaScript will throw an error if we try to assign
__proto__ in a circle.
2. The value of __proto__ can be either an object or null . Other types are ignored.
Also it may be obvious, but still: there can be only one [[Prototype]] . An object may not
inherit from two others.
let animal = {
eats: true,
walk() {
/* this method won't be used by rabbit */
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk = function() {
alert("Rabbit! Bounce-bounce!");
};
From now on, rabbit.walk() call finds the method immediately in the object and executes it,
without using the prototype:
animal
eats: true
walk: function
[[Prototype]]
rabbit
walk: function
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
};
// 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.
An interesting question may arise in the example above: what’s the value of this inside set
fullName(value) ? Where are the properties this.name and this.surname written: into
user or admin ?
No matter where the method is found: in an object or its prototype. In a method call, this
is always the object before the dot.
So, the setter call admin.fullName= uses admin as this , not user .
That is actually a super-important thing, because we may have a big object with many methods,
and have objects that inherit from it. And when the inheriting objects run the inherited methods,
they will modify only their own states, not the state of the big object.
For instance, here animal represents a “method storage”, and rabbit makes use of it.
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)
animal
walk: function
sleep: function
[[Prototype]]
rabbit
If we had other objects, like bird , snake , etc., inheriting from animal , they would also gain
access to methods of animal . But this in each method call would be the corresponding
object, evaluated at the call-time (before dot), not animal . So when we write data into this , it
is stored into these objects.
for…in loop
For instance:
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
If that’s not what we want, and we’d like to exclude inherited properties, there’s a built-in method
obj.hasOwnProperty(key) : 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):
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
if (isOwn) {
alert(`Our: ${prop}`); // Our: jumps
} else {
alert(`Inherited: ${prop}`); // Inherited: eats
}
}
Here we have the following inheritance chain: rabbit inherits from animal , that inherits from
Object.prototype (because animal is a literal object {...} , so it’s by default), and then
null above it:
null
[[Prototype]]
Object.prototype
toString: function
hasOwnProperty: function
...
[[Prototype]]
animal
eats: true
[[Prototype]]
rabbit
jumps: true
Note, there’s one funny thing. Where is the method rabbit.hasOwnProperty coming from?
We did not define it. Looking at the chain we can see that the method is provided by
Object.prototype.hasOwnProperty . In other words, it’s inherited.
…But why does hasOwnProperty not appear in the for..in loop like eats and jumps
do, if for..in lists inherited properties?
The answer is simple: it’s not enumerable. Just like all other properties of Object.prototype ,
it has enumerable:false flag. And for..in only lists enumerable properties. That’s why it
and the rest of the Object.prototype properties are not listed.
They only operate on the object itself. Properties from the prototype are not taken into
account.
Summary
●
In JavaScript, all objects have a hidden [[Prototype]] property that’s either another
object or null .
●
We can use obj.__proto__ to access it (a historical getter/setter, there are other ways, to
be covered soon).
●
The object referenced by [[Prototype]] is called a “prototype”.
●
If we want to read a property of obj or call a method, and it doesn’t exist, then JavaScript
tries to find it in the prototype.
● Write/delete operations act directly on the object, they don’t use the prototype (assuming it’s a
data property, not a setter).
●
If we call obj.method() , and the method is taken from the prototype, this still
references obj . So methods always work with the current object even if they are inherited.
●
The for..in loop iterates over both its own and its inherited properties. All other key/value-
getting methods only operate on the object itself.
✔ Tasks
Here’s the code that creates a pair of objects, then modifies them.
let animal = {
jumps: null
};
let rabbit = {
__proto__: animal,
jumps: true
};
delete rabbit.jumps;
delete animal.jumps;
To solution
Searching algorithm
importance: 5
let head = {
glasses: 1
};
let table = {
pen: 3
};
let bed = {
sheet: 1,
pillow: 2
};
let pockets = {
money: 2000
};
1. Use __proto__ to assign prototypes in a way that any property lookup will follow the path:
pockets → bed → table → head . For instance, pockets.pen should be 3 (found in
table ), and bed.glasses should be 1 (found in head ).
2. Answer the question: is it faster to get glasses as pockets.glasses or
head.glasses ? Benchmark if needed.
To solution
If we call rabbit.eat() , which object receives the full property: animal or rabbit ?
let animal = {
eat() {
this.full = true;
}
};
let rabbit = {
__proto__: animal
};
rabbit.eat();
To solution
We have two hamsters: speedy and lazy inheriting from the general hamster object.
When we feed one of them, the other one is also full. Why? How can we fix it?
let hamster = {
stomach: [],
eat(food) {
this.stomach.push(food);
}
};
let speedy = {
__proto__: hamster
};
let lazy = {
__proto__: hamster
};
To solution
F.prototype
Remember, new objects can be created with a constructor function, like new F() .
If F.prototype is an object, then the new operator uses it to set [[Prototype]] for the
new object.
Please note:
JavaScript had prototypal inheritance from the beginning. It was one of the core features of
the language.
But in the old times, there was no direct access to it. The only thing that worked reliably was a
"prototype" property of the constructor function, described in this chapter. So there are
many scripts that still use it.
Please note that F.prototype here means a regular property named "prototype" on F . It
sounds something similar to the term “prototype”, but here we really mean a regular property with
this name.
Here’s the example:
let animal = {
eats: true
};
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype = animal;
Setting Rabbit.prototype = animal literally states the following: "When a new Rabbit
is created, assign its [[Prototype]] to animal ".
[[Prototype]]
rabbit
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:
function Rabbit() {}
/* default prototype
Rabbit.prototype = { constructor: Rabbit };
*/
function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }
constructor
[[Prototype]]
rabbit
We can use constructor property to create a new object using the same constructor as the
existing one.
Like here:
function Rabbit(name) {
this.name = name;
alert(name);
}
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.
Yes, it exists in the default "prototype" for functions, but that’s all. What happens with it later
– is totally on us.
For instance:
function Rabbit() {}
Rabbit.prototype = {
jumps: true
};
function Rabbit() {}
Rabbit.prototype = {
jumps: true,
constructor: Rabbit
};
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.
let user = {
name: "John",
prototype: "Bla-bla" // no magic at all
};
✔ Tasks
Changing "prototype"
importance: 5
In the code below we create new Rabbit , and then try to modify its prototype.
In the start, we have this code:
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
1.
We added one more string (emphasized). What will alert show now?
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
Rabbit.prototype = {};
alert( rabbit.eats ); // ?
2.
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
Rabbit.prototype.eats = false;
alert( rabbit.eats ); // ?
3.
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
delete rabbit.eats;
alert( rabbit.eats ); // ?
4.
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
delete Rabbit.prototype.eats;
alert( rabbit.eats ); // ?
To solution
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.
Give an example of a constructor function for obj which lets such code work right. And an
example that makes it work wrong.
To solution
Native prototypes
The "prototype" property is widely used by the core of JavaScript itself. All built-in
constructor functions use it.
First we’ll see at the details, and then how to use it for adding new capabilities to built-in objects.
Object.prototype
…But the short notation obj = {} is the same as obj = new Object() , where Object
is a built-in object constructor function, with its own prototype referencing a huge object with
toString and other methods.
When new Object() is called (or a literal object {...} is created), the [[Prototype]] of
it is set to Object.prototype according to the rule that we discussed in the previous chapter:
[[Prototype]]
Please note that there is no more [[Prototype]] in the chain above Object.prototype :
alert(Object.prototype.__proto__); // null
Other built-in objects such as Array , Date , Function and others also keep methods in
prototypes.
For instance, when we create an array [1, 2, 3] , the default new Array() constructor is
used internally. So Array.prototype becomes its prototype and provides methods. That’s
very memory-efficient.
By specification, all of the built-in prototypes have Object.prototype on the top. That’s why
some people say that “everything inherits from objects”.
Here’s the overall picture (for 3 built-ins to fit):
null
[[Prototype]]
Object.prototype
toString: function
other object methods
[[Prototype]] [[Prototype]]
[[Prototype]]
Some methods in prototypes may overlap, for instance, Array.prototype has its own
toString that lists comma-delimited elements:
toString: function
...
[[Prototype]]
Array.prototype
toString: function
...
[[Prototype]]
[1, 2, 3]
In-browser tools like Chrome developer console also show inheritance ( console.dir may
need to be used for built-in objects):
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.
function f() {}
Primitives
The most intricate thing happens with strings, numbers and booleans.
As we remember, they are not objects. But if we try to access their properties, temporary wrapper
objects are created using built-in constructors String , Number and Boolean . They provide
the methods and disappear.
These objects are created invisibly to us and most engines optimize them out, but the
specification describes it exactly this way. Methods of these objects also reside in prototypes,
available as String.prototype , Number.prototype and Boolean.prototype .
⚠ Values null and undefined have no object wrappers
Special values null and undefined stand apart. They have no object wrappers, so
methods and properties are not available for them. And there are no corresponding
prototypes either.
String.prototype.show = function() {
alert(this);
};
"BOOM!".show(); // BOOM!
During the process of development, we may have ideas for new built-in methods we’d like to
have, and we may be tempted to add them to native prototypes. But that is generally a bad idea.
⚠ Important:
Prototypes are global, so it’s easy to get a conflict. If two libraries add a method
String.prototype.show , then one of them will be overwriting the method of the other.
In modern programming, there is only one case where modifying native prototypes is
approved. That’s polyfilling.
Polyfilling is a term for making a substitute for a method that exists in the JavaScript specification,
but is not yet supported by a particular JavaScript engine.
We may then implement it manually and populate the built-in prototype with it.
For instance:
String.prototype.repeat = function(n) {
// repeat the string n times
// actually, the code should be a little bit more complex than that
// (the full algorithm is in the specification)
// but even an imperfect polyfill is often considered good enough
return new Array(n + 1).join(this);
};
}
In the chapter Decorators and forwarding, call/apply we talked about method borrowing.
That’s when we take a method from one object and copy it into another.
E.g.
let obj = {
0: "Hello",
1: "world!",
length: 2,
};
obj.join = Array.prototype.join;
It works because the internal algorithm of the built-in join method only cares about the correct
indexes and the length property. It doesn’t check if the object is indeed an array. Many built-in
methods are like that.
But that’s impossible if obj already inherits from another object. Remember, we only can inherit
from one object at a time.
Borrowing methods is flexible, it allows to mix functionalities from different objects if needed.
Summary
●
All built-in objects follow the same pattern:
●
The methods are stored in the prototype ( Array.prototype , Object.prototype ,
Date.prototype , etc.)
● The object itself stores only the data (array items, object properties, the date)
●
Primitives also store methods in prototypes of wrapper objects: Number.prototype ,
String.prototype and Boolean.prototype . Only undefined and null do not
have wrapper objects
●
Built-in prototypes can be modified or populated with new methods. But it’s not recommended
to change them. The only allowable case is probably when we add-in a new standard, but it’s
not yet supported by the JavaScript engine
✔ Tasks
Add to the prototype of all functions the method defer(ms) , that runs the function after ms
milliseconds.
function f() {
alert("Hello!");
}
To solution
Add to the prototype of all functions the method defer(ms) , that returns a wrapper, delaying
the call by ms milliseconds.
function f(a, b) {
alert( a + b );
}
Please note that the arguments should be passed to the original function.
To solution
The __proto__ is considered outdated and somewhat deprecated (in browser-only part of the
JavaScript standard).
let animal = {
eats: true
};
alert(rabbit.eats); // true
let animal = {
eats: true
};
alert(rabbit.jumps); // true
The descriptors are in the same format as described in the chapter Property flags and
descriptors.
We can use Object.create to perform an object cloning more powerful than copying
properties in for..in :
This call makes a truly exact copy of obj , including all properties: enumerable and non-
enumerable, data properties and setters/getters – everything, and with the right
[[Prototype]] .
Brief history
If we count all the ways to manage [[Prototype]] , there are a lot! Many ways to do the
same!
Why?
And JavaScript engines are highly optimized for this. Changing a prototype “on-the-fly” with
Object.setPrototypeOf or obj.__proto__= is a very slow operation as it breaks
internal optimizations for object property access operations. So avoid it unless you know what
you’re doing, or JavaScript speed totally doesn’t matter for you.
…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__" .
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 didn’t intend to implement such behavior, right? We want to store key/value pairs, and the
key named "__proto__" was not properly saved. So that’s a bug!
Here the consequences are not terrible. But in other cases we may be assigning object values,
and then the prototype may indeed be changed. As a result, the execution will go wrong in totally
unexpected ways.
What’s worse – usually developers do not think about such possibility at all. That makes such
bugs hard to notice and even turn them into vulnerabilities, especially when JavaScript is used on
server-side.
Unexpected things also may happen when assigning to toString , which is a function by
default, and to other built-in methods.
But Object can also serve us well here, because language creators gave thought to that
problem long ago.
[[Prototype]]
obj
So, if obj.__proto__ is read or set, the corresponding getter/setter is called from its
prototype, and it gets/sets [[Prototype]] .
As it was said in the beginning of this tutorial section: __proto__ is a way to access
[[Prototype]] , it is not [[Prototype]] itself.
Now, if we want to use an object as an associative array, we can do it with a little trick:
null
[[Prototype]]
obj
So, there is no inherited getter/setter for __proto__ . Now it is processed as a regular data
property, so the example above works right.
We can call such objects “very plain” or “pure dictionary” objects, because they are even simpler
than the regular plain object {...} .
A downside is that such objects lack any built-in object methods, e.g. toString :
alert(Object.keys(chineseDictionary)); // hello,bye
Summary
The built-in __proto__ getter/setter is unsafe if we’d want to put user-generated keys into an
object. Just because a user may enter "__proto__" as the key, and there’ll be an error, with
hopefully light, but generally unpredictable consequences.
Also, Object.create provides an easy way to shallow-copy an object with all descriptors:
We also made it clear that __proto__ is a getter/setter for [[Prototype]] and resides in
Object.prototype , just like other methods.
All methods that return object properties (like Object.keys and others) – return “own”
properties. If we want inherited ones, we can use for..in .
✔ Tasks
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.
To solution
rabbit.sayHi();
Rabbit.prototype.sayHi();
Object.getPrototypeOf(rabbit).sayHi();
rabbit.__proto__.sayHi();
To solution
Classes
Class basic syntax
In practice, we often need to create many objects of the same kind, like users, or goods or
whatever.
As we already know from the chapter Constructor, operator "new", new function can help
with that.
But in the modern JavaScript, there’s a more advanced “class” construct, that introduces great
new features which are useful for object-oriented programming.
class MyClass {
// class methods
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}
Then use new MyClass() to create a new object with all the listed methods.
The constructor() method is called automatically by new , so we can initialize the object
there.
For example:
class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
// Usage:
let user = new User("John");
user.sayHi();
The notation here is not to be confused with object literals. Within the class, no commas are
required.
What is a class?
So, what exactly is a class ? That’s not an entirely new language-level entity, as one might
think.
Let’s unveil any magic and see what a class really is. That’ll help in understanding many complex
aspects.
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// proof: User is a function
alert(typeof User); // function
1. Creates a function named User , that becomes the result of the class declaration. The
function code is taken from the constructor method (assumed empty if we don’t write such
method).
2. Stores class methods, such as sayHi , in User.prototype .
After new User object is created, when we call its method, it’s taken from the prototype, just as
described in the chapter F.prototype. So the object has access to class methods.
User User.prototype
prototype
sayHi: function
constructor: User
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// class is a function
alert(typeof User); // function
Sometimes people say that class is a “syntactic sugar” (syntax that is designed to make things
easier to read, but doesn’t introduce anything new), because we could actually declare the same
without class keyword at all:
// Usage:
let user = new User("John");
user.sayHi();
The result of this definition is about the same. So, there are indeed reasons why class can be
considered a syntactic sugar to define a constructor together with its prototype methods.
And unlike a regular function, a class constructor must be called with new :
class User {
constructor() {}
}
Also, a string representation of a class constructor in most JavaScript engines starts with the
“class…”
class User {
constructor() {}
}
2. Class methods are non-enumerable. A class definition sets enumerable flag to false for
all methods in the "prototype" .
That’s good, because if we for..in over an object, we usually don’t want its class methods.
3. Classes always use strict . All code inside the class construct is automatically in strict
mode.
Besides, class syntax brings many other features that we’ll explore later.
Class Expression
Just like functions, classes can be defined inside another expression, passed around, returned,
assigned, etc.
Here’s an example of a class expression:
If a class expression has a name, it’s visible inside the class only:
function makeClass(phrase) {
// declare a class and return it
return class {
sayHi() {
alert(phrase);
};
};
}
Just like literal objects, classes may include getters/setters, computed properties etc.
class User {
constructor(name) {
// invokes the setter
this.name = name;
}
get name() {
return this._name;
}
set name(value) {
if (value.length < 4) {
alert("Name is too short.");
return;
}
this._name = value;
}
The class declaration creates getters and setters in User.prototype , like this:
Object.defineProperties(User.prototype, {
name: {
get() {
return this._name
},
set(name) {
// ...
}
}
});
class User {
['say' + 'Hi']() {
alert("Hello");
}
new User().sayHi();
Class properties
In the example above, User only had methods. Let’s add a property:
class User {
name = "Anonymous";
sayHi() {
alert(`Hello, ${this.name}!`);
}
}
new User().sayHi();
The property name is not placed into User.prototype . Instead, it is created by new before
calling the constructor, it’s a property of the object itself.
Summary
class MyClass {
prop = value; // property
constructor(...) { // constructor
// ...
}
method(...) {} // method
MyClass is technically a function (the one that we provide as constructor ), while methods,
getters and setters are written to MyClass.prototype .
In the next chapters we’ll learn more about classes, including inheritance and other features.
✔ Tasks
Rewrite to class
importance: 5
The Clock class is written in functional style. Rewrite it the “class” syntax.
Class inheritance
Class inheritance is a way for one class to extend another class.
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed += speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stands still.`);
}
}
Here’s how we can represent animal object and Animal class graphically:
Animal Animal.prototype
prototype
constructor: Animal
run: function
stop: function
[[Prototype]]
new Animal
name: "My animal"
As rabbits are animals, Rabbit class should be based on Animal , have access to animal
methods, so that rabbits can do what “generic” animals can do.
The syntax to extend another class is: class Child extends Parent .
Object of Rabbit class have access to both Rabbit methods, such as rabbit.hide() ,
and also to Animal methods, such as rabbit.run() .
Internally, extends keyword works using the good old prototype mechanics. It sets
Rabbit.prototype.[[Prototype]] to Animal.prototype . So, if a method is not found
in Rabbit.prototype , JavaScript takes it from Animal.prototype .
Animal Animal.prototype
constructor prototype
constructor: Animal
run: function
stop: function
extends
[[Prototype]]
Rabbit Rabbit.prototype
constructor prototype
constructor: Rabbit
hide: function
[[Prototype]]
new Rabbit
name: "White Rabbit"
For instance, to find rabbit.run method, the engine checks (bottom-up on the picture):
As we can recall from the chapter Native prototypes, JavaScript itself uses prototypal inheritance
for built-in objects. E.g. Date.prototype.[[Prototype]] is Object.prototype . That’s
why dates have access to generic object methods.
Any expression is allowed after extends
Class syntax allows to specify not just a class, but any expression after extends .
function f(phrase) {
return class {
sayHi() { alert(phrase) }
}
}
That may be useful for advanced programming patterns when we use functions to generate
classes depending on many conditions and can inherit from them.
Overriding a method
Now let’s move forward and override a method. By default, all methods that are not specified in
class Rabbit are taken directly “as is” from class Animal .
But if we specify our own method in Rabbit , such as stop() then it will be used instead:
Usually we don’t want to totally replace a parent method, but rather to build on top of it to tweak
or extend its functionality. We do something in our method, but call the parent method
before/after it or in the process.
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed += speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stands still.`);
}
stop() {
super.stop(); // call parent stop
this.hide(); // and then hide
}
}
Now Rabbit has the stop method that calls the parent super.stop() in the process.
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:
// Unexpected super
setTimeout(function() { super.stop() }, 1000);
Overriding constructor
With constructors it gets a little bit tricky.
According to the specification , if a class extends another class and has no constructor ,
then the following “empty” constructor is generated:
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 :
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
constructor(name, earLength) {
this.speed = 0;
this.name = name;
this.earLength = earLength;
}
// ...
}
// Doesn't work!
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.
Whoops! We’ve got an error. Now we can’t create rabbits. What went wrong?
The short answer is: constructors in inheriting classes must call super(...) , and (!) do it
before using this .
…But why? What’s going on here? Indeed, the requirement seems strange.
Of course, there’s an explanation. Let’s get into details, so you’ll really understand what’s going
on.
So a derived constructor must call super in order to execute its parent (non-derived)
constructor, otherwise the object for this won’t be created. And we’ll get an error.
For the Rabbit constructor to work, it needs to call super() before using this , like here:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
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
⚠ Advanced information
If you’re reading the tutorial for the first time – this section may be skipped.
Let’s get a little deeper under the hood of super . We’ll see some interesting things along the
way.
First to say, from all that we’ve learned till now, it’s impossible for super to work at all!
Yeah, indeed, let’s ask ourselves, how it should technically work? When an object method runs, it
gets the current object as this . If we call super.method() then, the engine needs to get the
method from the prototype of the current object. But how?
The task may seem simple, but it isn’t. The engine knows the current object this , so it could
get the parent method as this.__proto__.method . Unfortunately, such a “naive” solution
won’t work.
Let’s demonstrate the problem. Without classes, using plain objects for the sake of simplicity.
You may skip this part and go below to the [[HomeObject]] subsection if you don’t want to
know the details. That won’t harm. Or read on if you’re interested in understanding things in-
depth.
In the example below, rabbit.__proto__ = animal . Now let’s try: in rabbit.eat()
we’ll call animal.eat() , using this.__proto__ :
let animal = {
name: "Animal",
eat() {
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() {
// that's how super.eat() could presumably work
this.__proto__.eat.call(this); // (*)
}
};
At the line (*) we take eat from the prototype ( animal ) and call it in the context of the
current object. Please note that .call(this) is important here, because a simple
this.__proto__.eat() would execute parent eat in the context of the prototype, not the
current object.
And in the code above it actually works as intended: we have the correct alert .
Now let’s add one more object to the chain. We’ll see how things break:
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); // (**)
}
};
The code doesn’t work anymore! We can see the error trying to call longEar.eat() .
It may be not that obvious, but if we trace longEar.eat() call, then we can see why. In both
lines (*) and (**) the value of this is the current object ( longEar ). That’s essential: all
object methods get the current object as this , not a prototype or something.
So, in both lines (*) and (**) the value of this.__proto__ is exactly the same:
rabbit . They both call rabbit.eat without going up the chain in the endless loop.
rabbit longEar
rabbit longEar
2. Then in the line (*) of rabbit.eat , we’d like to pass the call even higher in the chain, but
this=longEar , so this.__proto__.eat is again rabbit.eat !
[[HomeObject]]
To provide the solution, JavaScript adds one more special internal property for functions:
[[HomeObject]] .
Then super uses it to resolve the parent prototype and its methods.
let animal = {
name: "Animal",
eat() { // animal.eat.[[HomeObject]] == animal
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() { // rabbit.eat.[[HomeObject]] == rabbit
super.eat();
}
};
let longEar = {
__proto__: rabbit,
name: "Long Ear",
eat() { // longEar.eat.[[HomeObject]] == longEar
super.eat();
}
};
// works correctly
longEar.eat(); // Long Ear eats.
The very existance of [[HomeObject]] violates that principle, because methods remember
their objects. [[HomeObject]] can’t be changed, so this bond is forever.
The only place in the language where [[HomeObject]] is used – is super . So, if a method
does not use super , then we can still consider it free and copy between objects. But with
super things may go wrong.
let plant = {
sayHi() {
console.log("I'm a plant");
}
};
animal plant
sayHi sayHi
rabbit tree
[[HomeObject]]
sayHi sayHi
The difference may be non-essential for us, but it’s important for JavaScript.
In the example below a non-method syntax is used for comparison. [[HomeObject]] property
is not set and the inheritance doesn’t work:
let animal = {
eat: function() { // intentially writing like this instead of eat() {...
// ...
}
};
let rabbit = {
__proto__: animal,
eat: function() {
super.eat();
}
};
Summary
Also:
●
Arrow functions don’t have their own this or super , so they transparently fit into the
surrounding context.
✔ Tasks
class Animal {
constructor(name) {
this.name = name;
}
To solution
Extended clock
importance: 5
We’ve got a Clock class. As of now, it prints the time every second.
class Clock {
constructor({ template }) {
this.template = template;
}
render() {
let date = new Date();
console.log(output);
}
stop() {
clearInterval(this.timer);
}
start() {
this.render();
this.timer = setInterval(() => this.render(), 1000);
}
}
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.
To solution
As we know, all objects normally inherit from Object.prototype and get access to “generic”
object methods like hasOwnProperty etc.
For instance:
class Rabbit {
constructor(name) {
this.name = name;
}
}
But if we spell it out explicitly like "class Rabbit extends Object" , then the result would
be different from a simple "class Rabbit" ?
Here’s an example of such code (it doesn’t work – why? fix it?):
To solution
class User {
static staticMethod() {
alert(this === User);
}
}
User.staticMethod(); // true
class User { }
User.staticMethod = function() {
alert(this === User);
};
User.staticMethod(); // true
The value of this in User.staticMethod() call is the class constructor User itself (the
“object before dot” rule).
Usually, static methods are used to implement functions that belong to the class, but not to any
particular object of it.
For instance, we have Article objects and need a function to compare them. A natural
solution would be to add Article.compare method, like this:
class Article {
constructor(title, date) {
this.title = title;
this.date = date;
}
// usage
let articles = [
new Article("HTML", new Date(2019, 1, 1)),
new Article("CSS", new Date(2019, 0, 1)),
new Article("JavaScript", new Date(2019, 11, 1))
];
articles.sort(Article.compare);
The first way can be implemented by the constructor. And for the second one we can make a
static method of the class.
class Article {
constructor(title, date) {
this.title = title;
this.date = date;
}
static createTodays() {
// remember, this = Article
return new this("Today's digest", new Date());
}
}
Now every time we need to create a today’s digest, we can call Article.createTodays() .
Once again, that’s not a method of an article, but a method of the whole class.
Static methods are also used in database-related classes to search/save/remove entries from the
database, like this:
Static properties
⚠ A recent addition
This is a recent addition to the language. Examples work in the recent Chrome.
Static properties are also possible, they look like regular class properties, but prepended by
static :
class Article {
static publisher = "Ilya Kantor";
}
For instance, Animal.compare and Animal.planet in the code below are inherited and
accessible as Rabbit.compare and Rabbit.planet :
class Animal {
static planet = "Earth";
constructor(name, speed) {
this.speed = speed;
this.name = name;
}
run(speed = 0) {
this.speed += speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
let rabbits = [
new Rabbit("White Rabbit", 10),
new Rabbit("Black Rabbit", 5)
];
rabbits.sort(Rabbit.compare);
alert(Rabbit.planet); // Earth
Now when we call Rabbit.compare , the inherited Animal.compare will be called.
How does it work? Again, using prototypes. As you might have already guessed, extends
gives Rabbit the [[Prototype]] reference to Animal .
Animal Animal.prototype
prototype
compare constructor: Animal
run: function
[[Prototype]] [[Prototype]]
Rabbit Rabbit.prototype
prototype
constructor: Rabbit
hide: function
[[Prototype]]
rabbit
class Animal {}
class Rabbit extends Animal {}
// for statics
alert(Rabbit.__proto__ === Animal); // true
Summary
Static methods are used for the functionality that belongs to the class “as a whole”. It doesn’t
relate to a concrete class instance.
Static properties are used when we’d like to store class-level data, also not bound to an instance.
class MyClass {
static property = ...;
static method() {
...
}
}
MyClass.property = ...
MyClass.method = ...
For class B extends A the prototype of the class B itself points to A : B.[[Prototype]]
= A . So if a field is not found in B , the search continues in A .
To understand this, let’s break away from development and turn our eyes into the real world.
Usually, devices that we’re using are quite complex. But delimiting the internal interface from the
external one allows to use them without problems.
A real-life example
For instance, a coffee machine. Simple from outside: a button, a display, a few holes…And,
surely, the result – great coffee! :)
Coffee machines are quite reliable, aren’t they? We can use one for years, and only if something
goes wrong – bring it for repairs.
The secret of reliability and simplicity of a coffee machine – all details are well-tuned and hidden
inside.
If we remove the protective cover from the coffee machine, then using it will be much more
complex (where to press?), and dangerous (it can electrocute).
But in order to hide inner details, we’ll use not a protective cover, but rather special syntax of the
language and conventions.
In object-oriented programming, properties and methods are split into two groups:
● Internal interface – methods and properties, accessible from other methods of the class, but
not from the outside.
●
External interface – methods and properties, accessible also from outside the class.
If we continue the analogy with the coffee machine – what’s hidden inside: a boiler tube, heating
element, and so on – is its internal interface.
An internal interface is used for the object to work, its details use each other. For instance, a
boiler tube is attached to the heating element.
But from the outside a coffee machine is closed by the protective cover, so that no one can reach
those. Details are hidden and inaccessible. We can use its features via the external interface.
So, all we need to use an object is to know its external interface. We may be completely unaware
how it works inside, and that’s great.
In many other languages there also exist “protected” fields: accessible only from inside the class
and those extending it (like private, but plus access from inheriting classes). They are also useful
for the internal interface. They are in a sense more widespread than private ones, because we
usually want inheriting classes to gain access to them.
Protected fields are not implemented in JavaScript on the language level, but in practice they are
very convenient, so they are emulated.
Now we’ll make a coffee machine in JavaScript with all these types of properties. A coffee
machine has a lot of details, we won’t model them to stay simple (though we could).
Protecting “waterAmount”
class CoffeeMachine {
waterAmount = 0; // the amount of water inside
constructor(power) {
this.power = power;
alert( `Created a coffee-machine, power: ${power}` );
}
// add water
coffeeMachine.waterAmount = 200;
Right now the properties waterAmount and power are public. We can easily get/set them
from the outside to any value.
Let’s change waterAmount property to protected to have more control over it. For instance, we
don’t want anyone to set it below zero.
That is not enforced on the language level, but there’s a well-known convention between
programmers that such properties and methods should not be accessed from the outside.
class CoffeeMachine {
_waterAmount = 0;
set waterAmount(value) {
if (value < 0) throw new Error("Negative water");
this._waterAmount = value;
}
get waterAmount() {
return this._waterAmount;
}
constructor(power) {
this._power = power;
}
// add water
coffeeMachine.waterAmount = -10; // Error: Negative water
Now the access is under control, so setting the water below zero fails.
Read-only “power”
For power property, let’s make it read-only. It sometimes happens that a property must be set at
creation time only, and then never modified.
That’s exactly the case for a coffee machine: power never changes.
To do so, we only need to make getter, but not the setter:
class CoffeeMachine {
// ...
constructor(power) {
this._power = power;
}
get power() {
return this._power;
}
But most of the time get.../set... functions are preferred, like this:
class CoffeeMachine {
_waterAmount = 0;
setWaterAmount(value) {
if (value < 0) throw new Error("Negative water");
this._waterAmount = value;
}
getWaterAmount() {
return this._waterAmount;
}
}
new CoffeeMachine().setWaterAmount(100);
That looks a bit longer, but functions are more flexible. They can accept multiple arguments
(even if we don’t need them right now).
On the other hand, get/set syntax is shorter, so ultimately there’s no strict rule, it’s up to you
to decide.
So protected fields are naturally inheritable. Unlike private ones that we’ll see below.
Private “#waterLimit”
⚠ A recent addition
This is a recent addition to the language. Not supported in JavaScript engines, or supported
partially yet, requires polyfilling.
There’s a finished JavaScript proposal, almost in the standard, that provides language-level
support for private properties and methods.
Privates should start with # . They are only accessible from inside the class.
For instance, here’s a private #waterLimit property and the water-checking private method
#checkWater :
class CoffeeMachine {
#waterLimit = 200;
#checkWater(value) {
if (value < 0) throw new Error("Negative water");
if (value > this.#waterLimit) throw new Error("Too much water");
}
On the language level, # is a special sign that the field is private. We can’t access it from outside
or from inheriting classes.
Private fields do not conflict with public ones. We can have both private #waterAmount and
public waterAmount fields at the same time.
class CoffeeMachine {
#waterAmount = 0;
get waterAmount() {
return this.#waterAmount;
}
set waterAmount(value) {
if (value < 0) throw new Error("Negative water");
this.#waterAmount = value;
}
}
machine.waterAmount = 100;
alert(machine.#waterAmount); // Error
Unlike protected ones, private fields are enforced by the language itself. That’s a good thing.
But if we inherit from CoffeeMachine , then we’ll have no direct access to #waterAmount .
We’ll need to rely on waterAmount getter/setter:
In many scenarios such limitation is too severe. If we extend a CoffeeMachine , we may have
legitimate reasons to access its internals. That’s why protected fields are used more often, even
though they are not supported by the language syntax.
class User {
...
sayHi() {
let fieldName = "name";
alert(`Hello, ${this[fieldName]}`);
}
}
With private fields that’s impossible: this['#name'] doesn’t work. That’s a syntax
limitation to ensure privacy.
Summary
In terms of OOP, delimiting of the internal interface from the external one is called encapsulation.
Protection for users, so that they don’t shoot themselves in the foot
Imagine, there’s a team of developers using a coffee machine. It was made by the “Best
CoffeeMachine” company, and works fine, but a protective cover was removed. So the internal
interface is exposed.
All developers are civilized – they use the coffee machine as intended. But one of them, John,
decided that he’s the smartest one, and made some tweaks in the coffee machine internals. So
the coffee machine failed two days later.
That’s surely not John’s fault, but rather the person who removed the protective cover and let
John do his manipulations.
The same in programming. If a user of a class will change things not intended to be changed
from the outside – the consequences are unpredictable.
Supportable
The situation in programming is more complex than with a real-life coffee machine, because we
don’t just buy it once. The code constantly undergoes development and improvement.
If we strictly delimit the internal interface, then the developer of the class can freely
change its internal properties and methods, even without informing the users.
If you’re a developer of such class, it’s great to know that private methods can be safely
renamed, their parameters can be changed, and even removed, because no external code
depends on them.
For users, when a new version comes out, it may be a total overhaul internally, but still simple to
upgrade if the external interface is the same.
Hiding complexity
People adore using things that are simple. At least from outside. What’s inside is a different thing.
It’s always convenient when implementation details are hidden, and a simple, well-
documented external interface is available.
Right now, private fields are not well-supported among browsers, but can be polyfilled.
Please note a very interesting thing. Built-in methods like filter , map and others – return
new objects of exactly the inherited type PowerArray . Their internal implementation uses the
object’s constructor property for that.
When arr.filter() is called, it internally creates the new array of results using exactly
arr.constructor , not basic Array . That’s actually very cool, because we can keep using
PowerArray methods further on the result.
If we’d like built-in methods like map or filter to return regular arrays, we can return Array
in Symbol.species , like here:
As you can see, now .filter returns Array . So the extended functionality is not passed any
further.
Built-in objects have their own static methods, for instance Object.keys , Array.isArray
etc.
As we already know, native classes extend each other. For instance, Array extends Object .
Normally, when one class extends another, both static and non-static methods are inherited. That
was thoroughly explained in the article Static properties and methods.
But built-in classes are an exception. They don’t inherit statics from each other.
For example, both Array and Date inherit from Object , so their instances have methods
from Object.prototype . But Array.[[Prototype]] does not reference Object , so
there’s no, for instance, Array.keys() (or Date.keys() ) static method.
[[Prototype]]
Date Date.prototype
prototype
now constructor: Date
parse toString: function
... getDate: function
...
[[Prototype]]
new Date()
1 Jan 2019
As you can see, there’s no link between Date and Object . They are independent, only
Date.prototype inherits from Object.prototype .
That’s an important difference of inheritance between built-in objects compared to what we get
with extends .
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.
It returns true if obj belongs to the Class or a class inheriting from it.
For instance:
class Rabbit {}
let rabbit = new Rabbit();
Please note that arr also belongs to the Object class. That’s because Array prototypically
inherits from Object .
Normally, instanceof examines the prototype chain for the check. We can also set a custom
logic in the static method Symbol.hasInstance .
For example:
2. Most classes do not have Symbol.hasInstance . In that case, the standard logic is used:
obj instanceOf Class checks whether Class.prototype is equal to one of the
prototypes in the obj prototype chain.
class Animal {}
class Rabbit extends Animal {}
null
[[Prototype]]
Object.prototype
[[Prototype]]
Animal.prototype
= Animal.prototype?
[[Prototype]]
Rabbit.prototype
[[Prototype]]
rabbit
By the way, there’s also a method objA.isPrototypeOf(objB) , that returns true if objA is
somewhere in the chain of prototypes for objB . So the test of obj instanceof Class can
be rephrased as Class.prototype.isPrototypeOf(obj) .
It’s funny, but the Class constructor itself does not participate in the check! Only the chain of
prototypes and Class.prototype matters.
That can lead to interesting consequences when a prototype property is changed after the
object is created.
Like here:
function Rabbit() {}
let rabbit = new Rabbit();
We already know that plain objects are converted to string as [object Object] :
That’s their implementation of toString . But there’s a hidden feature that makes toString
actually much more powerful than that. We can use it as an extended typeof and an
alternative for instanceof .
By specification , 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:
Here we used call as described in the chapter Decorators and forwarding, call/apply to
execute the function objectToString in the context this=arr .
Internally, the toString algorithm examines this and returns the corresponding result. More
examples:
let s = Object.prototype.toString;
Symbol.toStringTag
The behavior of Object toString can be customized using a special object property
Symbol.toStringTag .
For instance:
let user = {
[Symbol.toStringTag]: "User"
};
For most environment-specific objects, there is such a property. Here are some browser specific
examples:
As you can see, the result is exactly Symbol.toStringTag (if exists), wrapped into [object
...] .
At the end we have “typeof on steroids” that not only works for primitive data types, but also for
built-in objects and even can be customized.
We can use {}.toString.call instead of instanceof for built-in objects when we want to
get the type as a string rather than just to check.
Summary
And instanceof operator really shines when we are working with a class hierarchy and want
to check for the class taking into account inheritance.
✔ Tasks
Strange instanceof
importance: 5
In the code below, why does instanceof return true ? We can easily see that a is not
created by B() .
function A() {}
function B() {}
To solution
Mixins
In JavaScript we can only inherit from a single object. There can be only one [[Prototype]]
for an object. And a class may extend only one other class.
But sometimes that feels limiting. For instance, we have a class StreetSweeper and a class
Bicycle , and want to make their mix: a StreetSweepingBicycle .
Or we have a class User and a class EventEmitter that implements event generation, and
we’d like to add the functionality of EventEmitter to User , so that our users can emit events.
As defined in Wikipedia, a mixin is a class containing methods that can be used by other
classes without a need to inherit from it.
In other words, a mixin provides methods that implement a certain behavior, but we do not use it
alone, we use it to add the behavior to other classes.
A mixin example
The simplest way to implement a mixin in JavaScript is to make an object with useful methods, so
that we can easily merge them into a prototype of any class.
For instance here the mixin sayHiMixin is used to add some “speech” for User :
// mixin
let sayHiMixin = {
sayHi() {
alert(`Hello ${this.name}`);
},
sayBye() {
alert(`Bye ${this.name}`);
}
};
// usage:
class User {
constructor(name) {
this.name = name;
}
}
There’s no inheritance, but a simple method copying. So User may inherit from another class
and also include the mixin to “mix-in” the additional methods, like this:
Object.assign(User.prototype, sayHiMixin);
let sayMixin = {
say(phrase) {
alert(phrase);
}
};
let sayHiMixin = {
__proto__: sayMixin, // (or we could use Object.create to set the prototype here)
sayHi() {
// call parent method
super.say(`Hello ${this.name}`); // (*)
},
sayBye() {
super.say(`Bye ${this.name}`); // (*)
}
};
class User {
constructor(name) {
this.name = name;
}
}
sayMixin
say: function
[[Prototype]]
User.prototype sayHiMixin
constructor: User [[HomeObject] sayHi: function
sayHi: function sayBye: function
sayBye: function
[[Prototype]]
user
name: ...
That’s because methods sayHi and sayBye were initially created in sayHiMixin . So even
though they got copied, their [[HomeObject]] internal property references sayHiMixin , as
shown in the picture above.
EventMixin
An important feature of many browser objects (for instance) is that they can generate events.
Events are a great way to “broadcast information” to anyone who wants it. So let’s make a mixin
that allows us to easily add event-related functions to any class/object.
●
The mixin will provide a method .trigger(name, [...data]) to “generate an event”
when something important happens to it. The name argument is a name of the event,
optionally followed by additional arguments with event data.
●
Also the method .on(name, handler) that adds handler function as the listener to
events with the given name. It will be called when an event with the given name triggers, and
get the arguments from the .trigger call.
●
…And the method .off(name, handler) that removes the handler listener.
After adding the mixin, an object user will be able to generate an event "login" when the
visitor logs in. And another object, say, calendar may want to listen for such events to load the
calendar for the logged-in person.
Or, a menu can generate the event "select" when a menu item is selected, and other objects
may assign handlers to react on that event. And so on.
/**
* Cancel the subscription, usage:
* menu.off('select', handler)
*/
off(eventName, handler) {
let handlers = this._eventHandlers && this._eventHandlers[eventName];
if (!handlers) return;
for (let i = 0; i < handlers.length; i++) {
if (handlers[i] === handler) {
handlers.splice(i--, 1);
}
}
},
/**
* Generate an event with the given name and data
* this.trigger('select', data1, data2);
*/
trigger(eventName, ...args) {
if (!this._eventHandlers || !this._eventHandlers[eventName]) {
return; // no handlers for that event name
}
● .on(eventName, handler) – assigns function handler to run when the event with that
name occurs. Technically, there’s an _eventHandlers property that stores an array of
handlers for each event name, and it just adds it to the list.
●
.off(eventName, handler) – removes the function from the handlers list.
●
.trigger(eventName, ...args) – generates the event: all handlers from
_eventHandlers[eventName] are called, with a list of arguments ...args .
Usage:
// Make a class
class Menu {
choose(value) {
this.trigger("select", value);
}
}
// Add the mixin with event-related methods
Object.assign(Menu.prototype, eventMixin);
// triggers the event => the handler above runs and shows:
// Value selected: 123
menu.choose("123");
Now, if we’d like any code to react to a menu selection, we can listen for it with menu.on(...) .
And eventMixin mixin makes it easy to add such behavior to as many classes as we’d like,
without interfering with the inheritance chain.
Summary
Mixin – is a generic object-oriented programming term: a class that contains methods for other
classes.
Some other languages allow multiple inheritance. JavaScript does not support multiple
inheritance, but mixins can be implemented by copying methods into prototype.
We can use mixins as a way to augment a class by adding multiple behaviors, like event-handling
as we have seen above.
Mixins may become a point of conflict if they accidentally overwrite existing class methods. So
generally one should think well about the naming methods of a mixin, to minimize the probability
of that happening.
Error handling
Error handling, "try..catch"
No matter how great we are at programming, sometimes our scripts have errors. They may occur
because of our mistakes, an unexpected user input, an erroneous server response, and for a
thousand other reasons.
But there’s a syntax construct try..catch that allows to “catch” errors and, instead of dying,
do something more reasonable.
The try..catch construct has two main blocks: try , and then catch :
try {
// code...
} catch (err) {
// error handling
Begin
try {
// code...
}
So, an error inside the try {…} block does not kill the script: we have a chance to handle it in
catch .
try {
} catch(err) {
}
●
An example with an error: shows (1) and (3) :
try {
} catch(err) {
It won’t work if the code is syntactically wrong, for instance it has unmatched curly braces:
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 phase are called “parse-time” errors and are unrecoverable (from inside that code).
That’s because the engine can’t understand the code.
So, try..catch can only handle errors that occur in the valid code. Such errors are called
“runtime errors” or, sometimes, “exceptions”.
⚠ try..catch works synchronously
If an exception happens in “scheduled” code, like in setTimeout , then try..catch
won’t catch it:
try {
setTimeout(function() {
noSuchVariable; // script will die here
}, 1000);
} catch (e) {
alert( "won't work" );
}
That’s because the function itself is executed later, when the engine has already left the
try..catch construct.
setTimeout(function() {
try {
noSuchVariable; // try..catch handles the error!
} catch {
alert( "error is caught here!" );
}
}, 1000);
Error object
When an error occurs, JavaScript generates an object containing the details about it. The object
is then passed as an argument to catch :
try {
// ...
} catch(err) { // <-- the "error object", could use another word instead of err
// ...
}
For all built-in errors, the error object has two main properties:
name
Error name. For instance, for an undefined variable that’s "ReferenceError" .
message
Textual message about error details.
There are other non-standard properties available in most environments. One of most widely
used and supported is:
stack
Current call stack: a string with information about the sequence of nested calls that led to the
error. Used for debugging purposes.
For instance:
try {
lalala; // error, variable is not defined!
} catch(err) {
alert(err.name); // ReferenceError
alert(err.message); // lalala is not defined
alert(err.stack); // ReferenceError: lalala is not defined at (...call stack)
⚠ A recent addition
This is a recent addition to the language. Old browsers may need polyfills.
try {
// ...
} catch { // <-- without (err)
// ...
}
Using “try…catch”
Usually it’s used to decode data received over the network, from the server or another source.
This way, if something’s wrong with the data, the visitor will never know that (unless they open
the developer console). And people really don’t like when something “just dies” without any error
message.
try {
} catch (e) {
// ...the execution jumps here
alert( "Our apologies, the data has errors, we'll try to request it one more time." );
alert( e.name );
alert( e.message );
}
Here we use the catch block only to show the message, but we can do much more: send a
new network request, suggest an alternative to the visitor, send information about the error to a
logging facility, … . All much better than just dying.
What if json is syntactically correct, but doesn’t have a required name property?
Like this:
try {
} catch (e) {
alert( "doesn't execute" );
}
Here JSON.parse runs normally, but the absence of name is actually an error for us.
“Throw” operator
The throw operator generates an error.
Technically, we can use anything as an error object. That may be even a primitive, like a number
or a string, but it’s better to use objects, preferably with name and message properties (to stay
somewhat compatible with built-in errors).
JavaScript has many built-in constructors for standard errors: Error , SyntaxError ,
ReferenceError , TypeError and others. We can use them to create error objects as well.
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:
alert(error.name); // Error
alert(error.message); // Things happen o_O
try {
JSON.parse("{ bad json o_O }");
} catch(e) {
alert(e.name); // SyntaxError
alert(e.message); // Unexpected token o in JSON at position 2
}
And in our case, the absence of name is an error, as users must have a name .
try {
if (!user.name) {
throw new SyntaxError("Incomplete data: no name"); // (*)
}
alert( user.name );
} catch(e) {
alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name
}
In the line (*) , the throw operator generates a SyntaxError with the given message , the
same way as JavaScript would generate it itself. The execution of try immediately stops and
the control flow jumps into catch .
Now catch became a single place for all error handling: both for JSON.parse and other
cases.
Rethrowing
In the example above we use try..catch to handle incorrect data. But is it possible that
another unexpected error occurs within the try {...} block? Like a programming error
(variable is not defined) or something else, not just this “incorrect data” thing.
For example:
try {
user = JSON.parse(json); // <-- forgot to put "let" before user
// ...
} catch(err) {
alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
// (no JSON Error actually)
}
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 :
try {
user = { /*...*/ };
} catch(e) {
alert(e.name); // "ReferenceError" for accessing an undefined variable
}
Catch should only process errors that it knows and “rethrow” all others.
In the code below, we use rethrowing so that catch only handles SyntaxError :
if (!user.name) {
throw new SyntaxError("Incomplete data: no name");
}
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 :
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
The try..catch construct may have one more code clause: finally .
try {
... try to execute the code ...
} catch(e) {
... handle errors ...
} finally {
... execute always ...
}
try {
alert( 'try' );
if (confirm('Make an error?')) BAD_CODE();
} catch (e) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
1. If you answer “Yes” to “Make an error?”, then try -> catch -> finally .
2. If you say “No”, then try -> finally .
The finally clause is often used when we start doing something and want to finalize it in any
case of outcome.
For instance, we want to measure the time that a Fibonacci numbers function fib(n) takes.
Naturally, we can start measuring before it runs and finish afterwards. But what if there’s an error
during the function call? In particular, the implementation of fib(n) in the code below returns
an error for negative or non-integer numbers.
The finally clause is a great place to finish the measurements no matter what.
Here finally guarantees that the time will be measured correctly in both situations – in case
of a successful execution of fib and in case of an error in it:
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);
}
try {
result = fib(num);
} catch (e) {
result = 0;
} finally {
diff = Date.now() - start;
}
You can check by running the code with entering 35 into prompt – it executes normally,
finally after try . And then enter -1 – there will be an immediate error, and the execution
will take 0ms . Both measurements are done correctly.
In other words, the function may finish with return or throw , that doesn’t matter. The
finally clause executes in both cases.
Otherwise, if we declared let in try block, it would only be visible inside of it.
finally and return
The finally clause works for any exit from try..catch . That includes an explicit
return .
In the example below, there’s a return in try . In this case, finally is executed just
before the control returns to the outer code.
function func() {
try {
return 1;
} catch (e) {
/* ... */
} finally {
alert( 'finally' );
}
}
alert( func() ); // first works alert from finally, and then this one
try..finally
The try..finally construct, without catch clause, is also useful. We apply it when we
don’t want to handle errors here (let them fall through), but want to be sure that processes
that we started are finalized.
function func() {
// start doing something that needs completion (like measurements)
try {
// ...
} finally {
// complete that thing even if all dies
}
}
In the code above, an error inside try always falls out, because there’s no catch . But
finally works before the execution flow leaves the function.
Global catch
⚠ Environment-specific
The information from this section is not a part of the core JavaScript.
Let’s imagine we’ve got a fatal error outside of try..catch , and the script died. Like a
programming error or some other terrible thing.
Is there a way to react on such occurrences? We may want to log the error, show something to
the user (normally they don’t see error messages), etc.
There is none in the specification, but environments usually provide it, because it’s really useful.
For instance, Node.js has process.on("uncaughtException") for that. And in the
browser we can assign a function to the special window.onerror property, that will run in case
of an uncaught error.
The syntax:
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:
<script>
window.onerror = function(message, url, line, col, error) {
alert(`${message}\n At ${line}:${col} of ${url}`);
};
function readData() {
badFunc(); // Whoops, something went wrong!
}
readData();
</script>
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
https://errorception.com or http://www.muscula.com .
1. We register at the service and get a piece of JS (or a script URL) from them to insert on pages.
2. That JS script sets a custom window.onerror function.
3. When an error occurs, it sends a network request about it to the service.
4. We can log in to the service web interface and see errors.
Summary
The try..catch construct allows to handle runtime errors. It literally allows to “try” running the
code and “catch” errors that may occur in it.
The syntax is:
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
}
If an error object is not needed, we can omit it by using catch { instead of catch(err) { .
We can also generate our own errors using the throw operator. Technically, the argument of
throw can be anything, but usually it’s an error object inheriting from the built-in Error class.
More on extending errors in the next chapter.
Rethrowing is a very important pattern of error handling: a catch block usually expects and
knows how to handle the particular error type, so it should rethrow errors it doesn’t know.
Even if we don’t have try..catch , most environments allow us to setup a “global” error
handler to catch errors that “fall out”. In-browser, that’s window.onerror .
✔ Tasks
1.
The first one uses finally to execute the code after try..catch :
try {
work work
} catch (e) {
handle errors
} finally {
cleanup the working space
}
2.
try {
work work
} catch (e) {
handle errors
}
We definitely need the cleanup after the work, doesn’t matter if there was an error or not.
Is there an advantage here in using finally or both code fragments are equal? If there is such
an advantage, then give an example when it matters.
To solution
Our errors should support basic error properties like message , name and, preferably, stack .
But they also may have other properties of their own, e.g. HttpError objects may have a
statusCode property with a value like 404 or 403 or 500 .
JavaScript allows to use throw with any argument, so technically our custom error classes
don’t need to inherit from Error . But if we inherit, then it becomes possible to use obj
instanceof Error to identify error objects. So it’s better to inherit from it.
As the application grows, our own errors naturally form a hierarchy. For instance,
HttpTimeoutError may inherit from HttpError , and so on.
Extending Error
As an example, let’s consider a function readUser(json) that should read JSON with user
data.
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 here’s its approximate code so we can understand what we’re
extending:
// 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 = <call stack>; // non-standard, but most environments support it
}
}
function test() {
throw new ValidationError("Whoops!");
}
try {
test();
} catch(err) {
alert(err.message); // Whoops!
alert(err.name); // ValidationError
alert(err.stack); // a list of nested calls with line numbers for each
}
Please note: in the line (1) we call the parent constructor. JavaScript requires us to call super
in the child constructor, so that’s obligatory. The parent constructor sets the message property.
The parent constructor also sets the name property to "Error" , so in the line (2) we reset it
to the right value.
// 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;
}
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
(*) .
// ...
// instead of (err instanceof SyntaxError)
} else if (err.name == "SyntaxError") { // (*)
// ...
The instanceof version is much better, because in the future we are going to extend
ValidationError , make subtypes of it, like PropertyRequiredError . And
instanceof check will continue to work for new inheriting classes. So that’s future-proof.
Also it’s important that if catch meets an unknown error, then it rethrows it in the line (**) .
The catch block only knows how to handle validation and syntax errors, other kinds (due to a
typo in the code or other unknown ones) should fall through.
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.
// 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;
}
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.
Here’s the code with MyError and other custom error classes, simplified:
// name is correct
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
Now custom errors are much shorter, especially ValidationError , as we got rid of the
"this.name = ..." line in the constructor.
Wrapping exceptions
The purpose of the function readUser in the code above is “to read the user data”. There may
occur different kinds of errors in the process. Right now we have SyntaxError and
ValidationError , but in the future readUser function may grow and probably generate
other kinds of errors.
The code which calls readUser should handle these errors. Right now it uses multiple if s in
the catch block, that check the class and handle known errors and rethrow the unknown ones.
But if the readUser function generates several kinds of errors, then we should ask ourselves:
do we really want to check for all error types one-by-one in every code that calls readUser ?
Often the answer is “No”: the outer code wants to be “one level above all that”, it just wants to
have some kind of “data reading error” – why exactly it happened is often irrelevant (the error
message describes it). Or, even better, it could have a way to get the error details, but only if we
need to.
So let’s make a new class ReadError to represent such errors. If an error occurs inside
readUser , we’ll catch it there and generate ReadError . We’ll also keep the reference to the
original error in its cause property. Then the outer code will only have to check for
ReadError .
Here’s the code that defines ReadError and demonstrates its use in readUser and
try..catch :
function validateUser(user) {
if (!user.age) {
throw new PropertyRequiredError("age");
}
if (!user.name) {
throw new PropertyRequiredError("name");
}
}
function readUser(json) {
let user;
try {
user = JSON.parse(json);
} catch (err) {
if (err instanceof SyntaxError) {
throw new ReadError("Syntax Error", err);
} else {
throw err;
}
}
try {
validateUser(user);
} catch (err) {
if (err instanceof ValidationError) {
throw new ReadError("Validation Error", err);
} else {
throw err;
}
}
try {
readUser('{bad json}');
} catch (e) {
if (e instanceof ReadError) {
alert(e);
// Original error: SyntaxError: Unexpected token b in JSON at position 1
alert("Original error: " + e.cause);
} else {
throw e;
}
}
In the code above, readUser works exactly as described – catches syntax and validation errors
and throws ReadError errors instead (unknown errors are rethrown as usual).
So the outer code checks instanceof ReadError and that’s it. No need to list all possible
error types.
The approach is called “wrapping exceptions”, because we take “low level exceptions” and “wrap”
them into ReadError that is more abstract 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. We just need to take
care of the name property and don’t forget to call super .
●
We can use instanceof to check for particular errors. It also works with inheritance. But
sometimes we have an error object coming from a 3rd-party library and there’s no easy way to
get its class. Then name property can be used for such checks.
●
Wrapping exceptions is a widespread technique: a function handles low-level exceptions and
creates higher-level errors instead of various low-level ones. Low-level exceptions sometimes
become properties of that object like err.cause in the examples above, but that’s not strictly
required.
✔ Tasks
Create a class FormatError that inherits from the built-in SyntaxError class.
Usage example:
To solution
Promises, async/await
Introduction: callbacks
If you’re not familiar with these methods, and their usage in the examples is confusing, or if
you would just like to understand them better, you may want to read a few chapters from the
next part of the tutorial.
Many actions in JavaScript are asynchronous. In other words, we initiate them now, but they
finish later.
There are other real-world examples of asynchronous actions, e.g. loading scripts and modules
(we’ll cover them in later chapters).
Take a look at the function loadScript(src) , that loads a script with the given src :
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
It appends to the document the new, dynamically created, tag <script src="…"> . The
browser loads and executes it.
The script is executed “asynchronously”, as it starts loading now, but runs later, when the function
has already finished.
If there’s any code below loadScript(…) , it doesn’t wait until the script loading finishes.
loadScript('/my/script.js');
// the code below loadScript
// doesn't wait for the script loading to finish
// ...
Let’s say we need to use the new script as soon as it loads. It declares new functions, and we
want to run them.
But if we do that immediately after the loadScript(…) call, that wouldn’t work:
loadScript('/my/script.js'); // the script has "function newFunction() {…}"
Naturally, the browser probably didn’t have time to load the script. As of now, the loadScript
function doesn’t provide a way to track the load completion. The script loads and eventually runs,
that’s all. But we’d like to know when it happens, to use new functions and variables from that
script.
Let’s add a callback function as a second argument to loadScript that should execute
when the script loads:
document.head.append(script);
}
Now if we want to call new functions from the script, we should write that in the callback:
loadScript('/my/script.js', function() {
// the callback runs after the script is loaded
newFunction(); // so now it works
...
});
That’s the idea: the second argument is a function (usually anonymous) that runs when the action
is completed.
How can we load two scripts sequentially: the first one, and then the second one after it?
The natural solution would be to put the second loadScript call inside the callback, like this:
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
alert(`Cool, the second script is loaded`);
});
});
After the outer loadScript is complete, the callback initiates the inner one.
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
loadScript('/my/script3.js', function(script) {
// ...continue after all scripts are loaded
});
})
});
So, every new action is inside a callback. That’s fine for few actions, but not good for many, so
we’ll see other variants soon.
Handling errors
In the above examples we didn’t consider errors. What if the script loading fails? Our callback
should be able to react on that.
document.head.append(script);
}
It calls callback(null, script) for successful load and callback(error) otherwise.
The usage:
Once again, the recipe that we used for loadScript is actually quite common. It’s called the
“error-first callback” style.
1. The first argument of the callback is reserved for an error if it occurs. Then
callback(err) is called.
2. The second argument (and the next ones if needed) are for the successful result. Then
callback(null, result1, result2…) is called.
So the single callback function is used both for reporting errors and passing back results.
Pyramid of Doom
From the first look, it’s a viable way of asynchronous coding. And indeed it is. For one or maybe
two nested calls it looks fine.
But for multiple asynchronous actions that follow one after another we’ll have code like this:
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...continue after all scripts are loaded (*)
}
});
}
})
}
});
In the code above:
1. We load 1.js , then if there’s no error.
2. We load 2.js , then if there’s no error.
3. We load 3.js , then if there’s no error – do something else (*) .
As calls become more nested, the code becomes deeper and increasingly more difficult to
manage, especially if we have real code instead of ... that may include more loops, conditional
statements and so on.
The “pyramid” of nested calls grows to the right with every asynchronous action. Soon it spirals
out of control.
We can try to alleviate the problem by making every action a standalone function, like this:
loadScript('1.js', step1);
See? It does the same, and there’s no deep nesting now because we made every action a
separate top-level function.
It works, but the code looks like a torn apart spreadsheet. It’s difficult to read, and you probably
noticed that one needs to eye-jump between pieces while reading it. That’s inconvenient,
especially if the reader is not familiar with the code and doesn’t know where to eye-jump.
Also, the functions named step* are all of single use, they are created only to avoid the
“pyramid of doom.” No one is going to reuse them outside of the action chain. So there’s a bit of
namespace cluttering here.
Luckily, there are other ways to avoid such pyramids. One of the best ways is to use “promises,”
described in the next chapter.
✔ Tasks
Now let’s say we need not just a circle, but to show a message inside it. The message should
appear after the animation is complete (the circle is fully grown), otherwise it would look ugly.
In the solution of the task, the function showCircle(cx, cy, radius) draws the circle, but
gives no way to track when it’s ready.
Demo:
Click me
To solution
Promise
Imagine that you’re a top singer, and fans ask day and night for your upcoming single.
To get some relief, you promise to send it to them when it’s published. You give your fans a list.
They can fill in their email addresses, so that when the song becomes available, all subscribed
parties instantly receive it. And even if something goes very wrong, say, a fire in the studio, so
that you can’t publish the song, they will still be notified.
Everyone is happy: you, because the people don’t crowd you anymore, and fans, because they
won’t miss the single.
1. A “producing code” that does something and takes time. For instance, a code that loads the
data over a network. That’s a “singer”.
2. A “consuming code” that wants the result of the “producing code” once it’s ready. Many
functions may need that result. These are the “fans”.
3. A promise is a special JavaScript object that links the “producing code” and the “consuming
code” together. In terms of our analogy: this is the “subscription list”. The “producing code”
takes whatever time it needs to produce the promised result, and the “promise” makes that
result available to all of the subscribed code when it’s ready.
The analogy isn’t terribly accurate, because JavaScript promises are more complex than a simple
subscription list: they have additional features and limitations. But it’s fine to begin with.
The function passed to new Promise is called the executor. When new Promise is created,
it runs automatically. It contains the producing code, that should eventually produce a result. In
terms of the analogy above: the executor is the “singer”.
Its arguments resolve and reject are callbacks provided by JavaScript itself. Our code is
only inside the executor.
When the executor obtains the result, be it soon or late – doesn’t matter, it should call one of
these callbacks:
●
resolve(value) — if the job finished successfully, with result value .
●
reject(error) — if an error occurred, error is the error object.
So to summarize: the executor runs automatically, it should do a job and then call either
resolve or reject .
The promise object returned by new Promise constructor has internal properties:
●
state — initially "pending" , then changes to either "fulfilled" when resolve is
called or "rejected" when reject is called.
●
result — initially undefined , then changes to value when resolve(value) called
or error when reject(error) is called.
state: "fulfilled"
result: value
e)
new Promise(executor) e( valu
l v
reso
state: "pending"
result: undefined reje
ct(e
r ror
)
state: "rejected"
result: error
Here’s an example of a promise constructor and a simple executor function with “producing code”
that takes time (via setTimeout ):
// after 1 second signal that the job is done with the result "done"
setTimeout(() => resolve("done"), 1000);
});
2. The executor receives two arguments: resolve and reject — these functions are pre-
defined by the JavaScript engine. So we don’t need to create them. We should only call one of
them when ready.
After one second of “processing” the executor calls resolve("done") to produce the
result. This changes the state of the promise object:
new Promise(executor)
And now an example of the executor rejecting the promise with an error:
new Promise(executor)
To summarize, the executor should do a job (something that takes time usually) and then call
resolve or reject to change the state of the corresponding promise object.
A promise that is either resolved or rejected is called “settled”, as opposed to an initially “pending”
promise.
The idea is that a job done by the executor may have only one result or an error.
Also, resolve / reject expect only one argument (or none) and will ignore additional
arguments.
Reject with Error objects
In case something goes wrong, the executor should call reject . That can be done with any
type of argument (just like resolve ). But it is recommended to use Error objects (or
objects that inherit from Error ). The reasoning for that will soon become apparent.
For instance, this might happen when we start to do a job but then see that everything has
already been completed and cached.
A Promise object serves as a link between the executor (the “producing code” or “singer”) and the
consuming functions (the “fans”), which will receive the result or error. Consuming functions can
be registered (subscribed) using methods .then , .catch and .finally .
then
The most important, fundamental one is .then .
promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
);
The first argument of .then is a function that runs when the promise is resolved, and receives
the result.
The second argument of .then is a function that runs when the promise is rejected, and
receives the error.
For instance, here’s a reaction to a successfully resolved promise:
If we’re interested only in successful completions, then we can provide only one function
argument to .then :
catch
If we’re interested only in errors, then we can use null as the first argument: .then(null,
errorHandlingFunction) . Or we can use .catch(errorHandlingFunction) , which
is exactly the same:
finally
Just like there’s a finally clause in a regular try {...} catch {...} , there’s
finally in promises.
The call .finally(f) is similar to .then(f, f) in the sense that f always runs when the
promise is settled: be it resolve or reject.
finally is a good handler for performing cleanup, e.g. stopping our loading indicators, as they
are not needed anymore, no matter what the outcome is.
Like this:
It’s not exactly an alias of then(f,f) though. There are several important differences:
1. A finally handler has no arguments. In finally we don’t know whether the promise is
successful or not. That’s all right, as our task is usually to perform “general” finalizing
procedures.
2. A finally handler passes through results and errors to the next handler.
And here there’s an error in the promise, passed through finally to catch :
That’s very convenient, because finally is not meant to process a promise result. So it
passes it through.
We’ll talk more about promise chaining and result-passing between handlers in the next
chapter.
3. Last, but not least, .finally(f) is a more convenient syntax than .then(f, f) : no
need to duplicate the function f .
On settled promises handlers run immediately
If a promise is pending, .then/catch/finally handlers wait for it. Otherwise, if a
promise has already settled, they execute immediately:
Next, let’s see more practical examples of how promises can help us write asynchronous code.
Example: loadScript
We’ve got the loadScript function for loading a script from the previous chapter.
document.head.append(script);
}
The new function loadScript will not require a callback. Instead, it will create and return a
Promise object that resolves when the loading is complete. The outer code can add handlers
(subscribing functions) to it using .then :
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
document.head.append(script);
});
}
Usage:
Promises Callbacks
So promises give us better code flow and flexibility. But there’s more. We’ll see that in the next
chapters.
✔ Tasks
Re-resolve a promise?
promise.then(alert);
To solution
The function delay(ms) should return a promise. That promise should resolve after ms
milliseconds, so that we can add .then to it, like this:
function delay(ms) {
// your code
}
delay(3000).then(() => alert('runs after 3 seconds'));
To solution
Rewrite the showCircle function in the solution of the task Animated circle with callback so
that it returns a promise instead of accepting a callback.
Take the solution of the task Animated circle with callback as the base.
To solution
Promises chaining
Let’s return to the problem mentioned in the chapter Introduction: callbacks: we have a sequence
of asynchronous tasks to be done one after another. For instance, loading scripts. How can we
code it well?
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
The idea is that the result is passed through the chain of .then handlers.
As the result is passed along the chain of handlers, we can see a sequence of alert calls: 1
→ 2 → 4.
new Promise
resolve(1)
.then
return 2
.then
return 4
.then
The whole thing works, because a call to promise.then returns a promise, so that we can call
the next .then on it.
When a handler returns a value, it becomes the result of that promise, so the next .then is
called with it.
A classic newbie error: technically we can also add many .then to a single promise. This
is not chaining.
For example:
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
What we did here is just several handlers to one promise. They don’t pass the result to each
other, instead they process it independently.
new Promise
resolve(1)
All .then on the same promise get the same result – the result of that promise. So in the code
above all alert show the same: 1 .
In practice we rarely need multiple handlers for one promise. Chaining is used much more often.
Returning promises
In that case further handlers wait until it settles, and then get its result.
For instance:
}).then(function(result) {
alert(result); // 1
}).then(function(result) { // (**)
alert(result); // 2
}).then(function(result) {
alert(result); // 4
});
Here the first .then shows 1 and returns new Promise(…) in the line (*) . After one
second it resolves, and the result (the argument of resolve , here it’s result * 2 ) is passed
on to handler of the second .then . That handler is in the line (**) , it shows 2 and does the
same thing.
So the output is the same as in the previous example: 1 → 2 → 4, but now with 1 second delay
between alert calls.
Example: loadScript
Let’s use this feature with the promisified loadScript , defined in the previous chapter, to load
scripts one by one, in sequence:
loadScript("/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("/article/promise-chaining/three.js");
})
.then(function(script) {
// use functions declared in scripts
// to show that they indeed loaded
one();
two();
three();
});
loadScript("/article/promise-chaining/one.js")
.then(script => loadScript("/article/promise-chaining/two.js"))
.then(script => loadScript("/article/promise-chaining/three.js"))
.then(script => {
// scripts are loaded, we can use functions declared there
one();
two();
three();
});
Here each loadScript call returns a promise, and the next .then runs when it resolves.
Then it initiates the loading of the next script. So scripts are loaded one after another.
We can add more asynchronous actions to the chain. Please note that the code is still “flat”, it
grows down, not to the right. There are no signs of “pyramid of doom”.
loadScript("/article/promise-chaining/one.js").then(script1 => {
loadScript("/article/promise-chaining/two.js").then(script2 => {
loadScript("/article/promise-chaining/three.js").then(script3 => {
// this function has access to variables script1, script2 and script3
one();
two();
three();
});
});
});
This code does the same: loads 3 scripts in sequence. But it “grows to the right”. So we have the
same problem as with callbacks.
People who start to use promises sometimes don’t know about chaining, so they write it this way.
Generally, chaining is preferred.
Sometimes it’s ok to write .then directly, because the nested function has access to the outer
scope. In the example above the most nested callback has access to all variables script1 ,
script2 , script3 . But that’s an exception rather than a rule.
Thenables
To be precise, a handler may return not exactly a promise, but a so-called “thenable” object –
an arbitrary object that has a method .then . It will be treated the same way as a promise.
The idea is that 3rd-party libraries may implement “promise-compatible” objects of their own.
They can have an extended set of methods, but also be compatible with native promises,
because they implement .then .
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve); // function() { native code }
// resolve with this.num*2 after the 1 second
setTimeout(() => resolve(this.num * 2), 1000); // (**)
}
}
JavaScript checks the object returned by the .then handler in line (*) : if it has a callable
method named then , then it calls that method providing native functions resolve ,
reject as arguments (similar to an executor) and waits until one of them is called. In the
example above resolve(2) is called after 1 second (**) . Then the result is passed
further down the chain.
This feature allows us to integrate custom objects with promise chains without having to
inherit from Promise .
Bigger example: fetch
In frontend programming promises are often used for network requests. So let’s see an extended
example of that.
We’ll use the fetch method to load the information about the user from the remote server. It has a
lot of optional parameters covered in separate chapters, but the basic syntax is quite simple:
This makes a network request to the url and returns a promise. The promise resolves with a
response object when the remote server responds with headers, but before the full response is
downloaded.
To read the full response, we should call the method response.text() : it returns a promise
that resolves when the full text is downloaded from the remote server, with that text as a result.
The code below makes a request to user.json and loads its text from the server:
fetch('/article/promise-chaining/user.json')
// .then below runs when the remote server responds
.then(function(response) {
// response.text() returns a new promise that resolves with the full response text
// when it loads
return response.text();
})
.then(function(text) {
// ...and here's the content of the remote file
alert(text); // {"name": "iliakan", isAdmin: true}
});
There is also a method response.json() that reads the remote data and parses it as JSON.
In our case that’s even more convenient, so let’s switch to it.
We’ll also use arrow functions for brevity:
For instance, we can make one more requests to GitHub, load the user profile and show the
avatar:
The code works, see comments about the details. However, there’s a potential problem in it, a
typical error of those who begin to use promises.
Look at the line (*) : how can we do something after the avatar has finished showing and gets
removed? For instance, we’d like to show a form for editing that user or something else. As of
now, there’s no way.
To make the chain extendable, we need to return a promise that resolves when the avatar
finishes showing.
Like this:
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise(function(resolve, reject) { // (*)
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser); // (**)
}, 3000);
}))
// triggers after 3 seconds
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
That is, .then handler in line (*) now returns new Promise , that becomes settled only
after the call of resolve(githubUser) in setTimeout (**) .
That makes it possible to plan actions after it. Even if we don’t plan to extend the chain now, we
may need it later.
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}
function loadGithubUser(name) {
return fetch(`https://api.github.com/users/${name}`)
.then(response => response.json());
}
function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}
// Use them:
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
// ...
Summary
If a .then (or catch/finally , doesn’t matter) handler returns a promise, the rest of the
chain waits until it settles. When it does, its result (or error) is passed further.
state: "pending"
result: undefined
✔ Tasks
Promise: then versus catch
Are these code fragments equal? In other words, do they behave the same way in any
circumstances, for any handler functions?
promise.then(f1).catch(f2);
Versus:
promise.then(f1, f2);
To solution
For instance, in the code below the URL to fetch is wrong (no such site) and .catch handles
the error:
fetch('https://no-such-server.blabla') // rejects
.then(response => response.json())
.catch(err => alert(err)) // TypeError: failed to fetch (the text may vary)
As you can see, the .catch doesn’t have to be immediate. It may appear after one or maybe
several .then .
Or, maybe, everything is all right with the site, but the response is not valid JSON. The easiest
way to catch all errors is to append .catch to the end of chain:
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));
Normally, such .catch doesn’t trigger at all. But if any of the promises above rejects (a network
problem or invalid json or whatever), then it would catch it.
Implicit try…catch
The code of a promise executor and promise handlers has an "invisible try..catch " around it.
If an exception happens, it gets caught and treated as a rejection.
The "invisible try..catch " around the executor automatically catches the error and turns it
into rejected promise.
This happens not only in the executor function, but in its handlers as well. If we throw inside a
.then handler, that means a rejected promise, so the control jumps to the nearest error
handler.
Here’s an example:
This happens for all errors, not just those caused by the throw statement. For example, a
programming error:
The final .catch not only catches explicit rejections, but also occasional errors in the handlers
above.
Rethrowing
As we already noticed, .catch at the end of the chain is similar to try..catch . We may
have as many .then handlers as we want, and then use a single .catch at the end to handle
errors in all of them.
In a regular try..catch we can analyze the error and maybe rethrow it if can’t handle. The
same thing is possible for promises.
If we throw inside .catch , then the control goes to the next closest error handler. And if we
handle the error and finish normally, then it continues to the closest successful .then handler.
}).catch(function(error) {
Here the .catch block finishes normally. So the next successful .then handler is called.
In the example below we see the other situation with .catch . The handler (*) catches the
error and just can’t handle it (e.g. it only knows how to handle URIError ), so it throws it again:
}).catch(function(error) { // (*)
throw error; // throwing this or another error jumps to the next catch
}
}).then(function() {
/* doesn't run here */
}).catch(error => { // (**)
});
The execution jumps from the first .catch (*) to the next one (**) down the chain.
Unhandled rejections
What happens when an error is not handled? For instance, we forgot to append .catch to the
end of the chain, like here:
new Promise(function() {
noSuchFunction(); // Error here (no such function)
})
.then(() => {
// successful promise handlers, one or more
}); // without .catch at the end!
In case of an error, the promise becomes rejected, and the execution should jump to the closest
rejection handler. But there is none. So the error gets “stuck”. There’s no code to handle it.
In practice, just like with regular unhandled errors in code, it means that something has gone
terribly wrong.
What happens when a regular error occurs and is not caught by try..catch ? The script dies
with a message in the console. A similar thing happens with unhandled promise rejections.
The JavaScript engine tracks such rejections and generates a global error in that case. You can
see it in the console if you run the example above.
In the browser we can catch such errors using the event unhandledrejection :
window.addEventListener('unhandledrejection', function(event) {
// the event object has two special properties:
alert(event.promise); // [object Promise] - the promise that generated the error
alert(event.reason); // Error: Whoops! - the unhandled error object
});
new Promise(function() {
throw new Error("Whoops!");
}); // no catch to handle the error
If an error occurs, and there’s no .catch , the unhandledrejection handler triggers, and
gets the event object with the information about the error, so we can do something.
Usually such errors are unrecoverable, so our best way out is to inform the user about the
problem and probably report the incident to the server.
In non-browser environments like Node.js there are other ways to track unhandled errors.
Summary
●
.catch handles errors in promises of all kinds: be it a reject() call, or an error thrown in
a handler.
● We should place .catch exactly in places where we want to handle errors and know how to
handle them. The handler should analyze errors (custom error classes help) and rethrow
unknown ones (maybe they are programming mistakes).
● It’s ok not to use .catch at all, if there’s no way to recover from an error.
●
In any case we should have the unhandledrejection event handler (for browsers, and
analogs for other environments) to track unhandled errors and inform the user (and probably
our server) about them, so that our app never “just dies”.
✔ Tasks
Error in setTimeout
What do you think? Will the .catch trigger? Explain your answer.
To solution
Promise API
There are 5 static methods in the Promise class. We’ll quickly cover their use cases here.
Promise.all
Let’s say we want to run many promises to execute in parallel, and wait until all of them are
ready.
For instance, download several URLs in parallel and process the content when all are done.
Promise.all takes an array of promises (it technically can be any iterable, but is usually an
array) and returns a new promise.
The new promise resolves when all listed promises are settled and the array of their results
becomes its result.
For instance, the Promise.all below settles after 3 seconds, and then its result is an array
[1, 2, 3] :
Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
]).then(alert); // 1,2,3 when promises are ready: each promise contributes an array member
Please note that the order of the resulting array members is the same as in its source promises.
Even though the first promise takes the longest time to resolve, it’s still first in the array of results.
A common trick is to map an array of job data into an array of promises, and then wrap that into
Promise.all .
For instance, if we have an array of URLs, we can fetch them all like this:
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://api.github.com/users/jeresig'
];
A bigger example with fetching user information for an array of GitHub users by their names (we
could fetch an array of goods by their ids, the logic is identical):
Promise.all(requests)
.then(responses => {
// all responses are resolved successfully
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // shows 200 for every url
}
return responses;
})
// map array of responses into an array of response.json() to read their content
.then(responses => Promise.all(responses.map(r => r.json())))
// all JSON answers are parsed: "users" is the array of them
.then(users => users.forEach(user => alert(user.name)));
For instance:
Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!
Here the second promise rejects in two seconds. That leads to an immediate rejection of
Promise.all , so .catch executes: the rejection error becomes the outcome of the whole
Promise.all .
For example, if there are multiple fetch calls, like in the example above, and one fails,
other ones will still continue to execute, but Promise.all won’t watch them anymore. They
will probably settle, but the result will be ignored.
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
}),
2,
3
]).then(alert); // 1, 2, 3
Promise.allSettled
⚠ A recent addition
This is a recent addition to the language. Old browsers may need polyfills.
Promise.all rejects as a whole if any promise rejects. That’s good for “all or nothing” cases,
when we need all results to go on:
Promise.all([
fetch('/template.html'),
fetch('/style.css'),
fetch('/data.json')
]).then(render); // render method needs results of all fetches
Promise.allSettled waits for all promises to settle. The resulting array has:
●
{status:"fulfilled", value:result} for successful responses,
●
{status:"rejected", reason:error} for errors.
For example, we’d like to fetch the information about multiple users. Even if one request fails,
we’re still interested in the others.
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://no-such-url'
];
[
{status: 'fulfilled', value: ...response...},
{status: 'fulfilled', value: ...response...},
{status: 'rejected', reason: ...error object...}
]
Polyfill
If the browser doesn’t support Promise.allSettled , it’s easy to polyfill:
if(!Promise.allSettled) {
Promise.allSettled = function(promises) {
return Promise.all(promises.map(p => Promise.resolve(p).then(value => ({
state: 'fulfilled',
value
}), reason => ({
state: 'rejected',
reason
}))));
};
}
In this code, promises.map takes input values, turns them into promises (just in case a non-
promise was passed) with p => Promise.resolve(p) , and then adds .then handler to
every one.
Now we can use Promise.allSettled to get the results of all given promises, even if some
of them reject.
Promise.race
Similar to Promise.all , but waits only for the first settled promise, and gets its result (or
error).
Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
The first promise here was fastest, so it became the result. After the first settled promise “wins
the race”, all further results/errors are ignored.
Promise.resolve/reject
We cover them here for completeness, and for those who can’t use async/await for some
reason.
●
Promise.resolve(value) creates a resolved promise with the result value .
Same as:
let promise = new Promise(resolve => resolve(value));
The method is used for compatibility, when a function is expected to return a promise.
For example, loadCached function below fetches a URL and remembers (caches) its content.
For future calls with the same URL it immediately gets the previous content from cache, but uses
Promise.resolve to make a promise of it, so that the returned value is always a promise:
function loadCached(url) {
if (cache.has(url)) {
return Promise.resolve(cache.get(url)); // (*)
}
return fetch(url)
.then(response => response.text())
.then(text => {
cache.set(url,text);
return text;
});
}
Promise.reject
Same as:
Summary
1. Promise.all(promises) – waits for all promises to resolve and returns an array of their
results. If any of the given promises rejects, it becomes the error of Promise.all , and all
other results are ignored.
2. Promise.allSettled(promises) (recently added method) – waits for all promises to
settle and returns their results as an array of objects with:
●
state : "fulfilled" or "rejected"
●
value (if fulfilled) or reason (if rejected).
3. Promise.race(promises) – waits for the first promise to settle, and its result/error
becomes the outcome.
4. Promise.resolve(value) – makes a resolved promise with the given value.
5. Promise.reject(error) – makes a rejected promise with the given error.
Promisification
Promisification – is a long word for a simple transform. It’s conversion of a function that accepts a
callback into a function returning a promise.
Such transforms are often needed in real-life, as many functions and libraries are callback-based.
But promises are more convenient. So it makes sense to promisify those.
document.head.append(script);
}
// usage:
// loadScript('path/script.js', (err, script) => {...})
Let’s promisify it. The new loadScriptPromise(src) function will do the same, but accept
only src (no callback ) and return a promise.
// usage:
// loadScriptPromise('path/script.js').then(...)
As we can see, it delegates all the work to the original loadScript , providing its own callback
that translates to promise resolve/reject .
In practice we’ll probably need to promisify many functions, it makes sense to use a helper.
We’ll call it promisify(f) : it accepts a to-promisify function f and returns a wrapper function.
That wrapper does the same as in the code above: returns a promise and passes the call to the
original f , tracking the result in a custom callback:
function promisify(f) {
return function (...args) { // return a wrapper-function
return new Promise((resolve, reject) => {
function callback(err, result) { // our custom callback for f
if (err) {
return reject(err);
} else {
resolve(result);
}
}
// usage:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
Here we assume that the original function expects a callback with two arguments (err,
result) . That’s what we encounter most often. Then our custom callback is in exactly the right
format, and promisify works great for such a case.
But what if the original f expects a callback with more arguments callback(err, res1,
res2, ...) ?
args.push(callback);
f.call(this, ...args);
});
};
};
// usage:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...)
For more exotic callback formats, like those without err at all: callback(result) , we can
promisify such functions without using the helper, manually.
There are also modules with a bit more flexible promisification functions, e.g. es6-promisify . In
Node.js, there’s a built-in util.promisify function for that.
Please note:
Promisification is a great approach, especially when you use async/await (see the next
chapter), but not a total replacement for callbacks.
Remember, a promise may have only one result, but a callback may technically be called
many times.
So promisification is only meant for functions that call the callback once. Further calls will be
ignored.
Microtasks
Promise handlers .then / .catch / .finally are always asynchronous.
Even when a Promise is immediately resolved, the code on the lines below
.then / .catch / .finally will still execute before these handlers .
If you run it, you see code finished first, and then promise done! .
That’s strange, because the promise is definitely done from the beginning.
Microtasks queue
Asynchronous tasks need proper management. For that, the standard specifies an internal queue
PromiseJobs , more often referred to as “microtask queue” (v8 term).
If there’s a chain with multiple .then/catch/finally , then every one of them is executed
asynchronously. That is, it first gets queued, and executed when the current code is complete
and previously queued handlers are finished.
What if the order matters for us? How can we make code finished run after promise
done ?
Promise.resolve()
.then(() => alert("promise done!"))
.then(() => alert("code finished"));
Unhandled rejection
Remember the unhandledrejection event from the chapter Error handling with promises?
Now we can see exactly how JavaScript finds out that there was an unhandled rejection.
"Unhandled rejection" occurs when a promise error is not handled at the end of the
microtask queue.
Normally, if we expect an error, we add .catch to the promise chain to handle it:
…But if we forget to add .catch , then, after the microtask queue is empty, the engine triggers
the event:
let promise = Promise.reject(new Error("Promise Failed!"));
// Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));
Now, if you run it, we’ll see Promise Failed! first and then caught .
If we didn’t know about the microtasks queue, we could wonder: “Why did
unhandledrejection handler run? We did catch the error!”.
But now we understand that unhandledrejection is generated when the microtask queue is
complete: the engine examines promises and, if any of them is in “rejected” state, then the event
triggers.
In the example above, .catch added by setTimeout also triggers, but later, after
unhandledrejection has already occurred, so that doesn’t change anything.
Summary
Promise handling is always asynchronous, as all promise actions pass through the internal
“promise jobs” queue, also called “microtask queue” (v8 term).
So, .then/catch/finally handlers are always called after the current code is finished.
In most Javascript engines, including browsers and Node.js, the concept of microtasks is closely
tied with “event loop” and “macrotasks”. As these have no direct relation to promises, they are
covered in another part of the tutorial, in the chapter Event loop: microtasks and macrotasks.
Async/await
There’s a special syntax to work with promises in a more comfortable fashion, called
“async/await”. It’s surprisingly easy to understand and use.
Async functions
Let’s start with the async keyword. It can be placed before a function, like this:
For instance, this function returns a resolved promise with the result of 1 , let’s test it:
f().then(alert); // 1
f().then(alert); // 1
So, async ensures that the function returns a promise, and wraps non-promises in it. Simple
enough, right? But not only that. There’s another keyword, await , that works only inside
async functions, and it’s pretty cool.
Await
The syntax:
The keyword await makes JavaScript wait until that promise settles and returns its result.
let result = await promise; // wait until the promise resolves (*)
alert(result); // "done!"
}
f();
The function execution “pauses” at the line (*) and resumes when the promise settles, with
result becoming its result. So the code above shows “done!” in one second.
Let’s emphasize: await literally makes JavaScript wait until the promise settles, and then go on
with the result. That doesn’t cost any CPU resources, because the engine can do other jobs
meanwhile: execute other scripts, handle events etc.
It’s just a more elegant syntax of getting the promise result than promise.then , easier to read
and write.
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error
}
We will get this error if we do not put async before a function. As said, await only works
inside an async function .
Let’s take the showAvatar() example from the chapter Promises chaining and rewrite it using
async/await :
// wait 3 seconds
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
Pretty clean and easy to read, right? Much better than before.
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
Here’s a demo Thenable class, the await below accepts its instances:
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// resolve with this.num*2 after 1000ms
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
};
f();
If await gets a non-promise object with .then , it calls that method providing built-in
functions resolve , reject as arguments (just as it does for a regular Promise
executor). Then await waits until one of them is called (in the example above it happens in
the line (*) ) and then proceeds with the result.
Async class methods
To declare an async class method, just prepend it with async :
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1
The meaning is the same: it ensures that the returned value is a promise and enables
await .
Error handling
If a promise resolves normally, then await promise returns the result. But in case of a
rejection, it throws the error, just as if there were a throw statement at that line.
This code:
In real situations, the promise may take some time before it rejects. In that case there will be a
delay before await throws an error.
We can catch that error using try..catch , the same way as a regular throw :
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
In case of an error, the control jumps to the catch block. We can also wrap multiple lines:
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// catches errors both in fetch and response.json
alert(err);
}
}
f();
If we don’t have try..catch , then the promise generated by the call of the async function
f() becomes rejected. We can append .catch to handle it:
If we forget to add .catch there, then we get an unhandled promise error (viewable in the
console). We can catch such errors using a global event handler as described in the chapter
Error handling with promises.
In case of an error, it propagates as usual: from the failed promise to Promise.all , and
then becomes an exception that we can catch using try..catch around the call.
Summary
The await keyword before a promise makes JavaScript wait until that promise settles, and
then:
1. If it’s an error, the exception is generated, same as if throw error were called at that very
place.
2. Otherwise, it returns the result.
Together they provide a great framework to write asynchronous code that is easy both to read
and write.
✔ Tasks
Rewrite this example code from the chapter Promises chaining using async/await instead of
.then/catch :
function loadJson(url) {
return fetch(url)
.then(response => {
if (response.status == 200) {
return response.json();
} else {
throw new Error(response.status);
}
})
}
loadJson('no-such-user.json') // (3)
.catch(alert); // Error: 404
To solution
Below you can find the “rethrow” example from the chapter Promises chaining. Rewrite it using
async/await instead of .then/catch .
And get rid of the recursion in favour of a loop in demoGithubUser : with async/await that
becomes easy to do.
function loadJson(url) {
return fetch(url)
.then(response => {
if (response.status == 200) {
return response.json();
} else {
throw new HttpError(response);
}
})
}
return loadJson(`https://api.github.com/users/${name}`)
.then(user => {
alert(`Full name: ${user.name}.`);
return user;
})
.catch(err => {
if (err instanceof HttpError && err.response.status == 404) {
alert("No such user, please reenter.");
return demoGithubUser();
} else {
throw err;
}
});
}
demoGithubUser();
To solution
We have a “regular” function. How to call async from it and use its result?
return 10;
}
function f() {
// ...what to write here?
// we need to call async wait() and wait to get 10
// remember, we can't use "await"
}
P.S. The task is technically very simple, but the question is quite common for developers new to
async/await.
To solution
Generators can return (“yield”) multiple values, one after another, on-demand. They work great
with iterables, allowing to create data streams with ease.
Generator functions
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
Generator functions behave differently from regular ones. When such function is called, it doesn’t
run its code. Instead it returns a special object, called “generator object”, to manage the
execution.
The main method of a generator is next() . When called, it runs the execution until the nearest
yield <value> statement ( value can be omitted, then it’s undefined ). Then the function
execution pauses, and the yielded value is returned to the outer code.
For instance, here we create the generator and get its first yielded value:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
As of now, we got the first value only, and the function execution is on the second line:
And, if we call it a third time, the execution reaches the return statement that finishes the
function:
Now the generator is done. We should see it from done:true and process value:3 as the
final result.
New calls to generator.next() don’t make sense any more. If we do them, they return the
same object: {done: true} .
As you probably already guessed looking at the next() method, generators are iterable.
…But please note: the example above shows 1 , then 2 , and that’s all. It doesn’t show 3 !
It’s because for..of iteration ignores the last value , when done: true . So, if we want all
results to be shown by for..of , we must return them with yield :
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
As generators are iterable, we can call all related functionality, e.g. the spread operator ... :
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
alert(sequence); // 0, 1, 2, 3
In the code above, ...generateSequence() turns the iterable generator object into an array
of items (read more about the spread operator in the chapter Rest parameters and spread
operator)
Some time ago, in the chapter Iterables we created an iterable range object that returns values
from..to .
Here, let’s remember the code:
let range = {
from: 1,
to: 5,
let range = {
from: 1,
to: 5,
That’s not a coincidence, of course. Generators were added to JavaScript language with iterators
in mind, to implement them easily.
The variant with a generator is much more concise than the original iterable code of range , and
keeps the same functionality.
That surely would require a break (or return ) in for..of over such generator.
Otherwise, the loop would repeat forever and hang.
Generator composition
We can use this sequence e.g. to create passwords by selecting characters from it (could add
syntax characters as well), but let’s generate it first.
In a regular function, to combine results from multiple other functions, we call them, store the
results, and then join at the end.
For generators, there’s a special yield* syntax to “embed” (compose) one generator into
another.
function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57);
// A..Z
yield* generateSequence(65, 90);
// a..z
yield* generateSequence(97, 122);
}
alert(str); // 0..9A..Za..z
The yield* directive delegates the execution to another generator. This term means that
yield* gen iterates over the generator gen and transparently forwards its yields outside. As
if the values were yielded by the outer generator.
The result is the same as if we inlined the code from nested generators:
function* generateAlphaNum() {
alert(str); // 0..9A..Za..z
A generator composition is a natural way to insert a flow of one generator into another. It doesn’t
use extra memory to store intermediate results.
Until this moment, generators were similar to iterable objects, with a special syntax to generate
values. But in fact they are much more powerful and flexible.
That’s because yield is a two-way road: it not only returns the result outside, but also can pass
the value inside the generator.
function* gen() {
// Pass a question to the outer code and wait for an answer
let result = yield "2 + 2 = ?"; // (*)
alert(result);
}
.next(4)
1. The first call generator.next() should be always made without an argument (the
argument is ignored if passed). It starts the execution and returns the result of the first yield
"2+2=?" . At this point the generator pauses the execution, while staying on the line (*) .
2. Then, as shown at the picture above, the result of yield gets into the question variable in
the calling code.
3. On generator.next(4) , the generator resumes, and 4 gets in as the result: let
result = 4 .
Please note, the outer code does not have to immediately call next(4) . It may take time. That’s
not a problem: the generator will wait.
For instance:
As we can see, unlike regular functions, a generator and the calling code can exchange results
by passing values in next/yield .
To make things more obvious, here’s another example, with more calls:
function* gen() {
let ask1 = yield "2 + 2 = ?";
alert(ask1); // 4
let ask2 = yield "3 * 3 = ?"
alert(ask2); // 9
}
"2 + 2 = ?"
. next ( 4)
"3 * 3 = ?"
. next ( 9)
1. The first .next() starts the execution… It reaches the first yield .
2. The result is returned to the outer code.
3. The second .next(4) passes 4 back to the generator as the result of the first yield , and
resumes the execution.
4. …It reaches the second yield , that becomes the result of the generator call.
5. The third next(9) passes 9 into the generator as the result of the second yield and
resumes the execution that reaches the end of the function, so done: true .
It’s like a “ping-pong” game. Each next(value) (excluding the first one) passes a value into
the generator, that becomes the result of the current yield , and then gets back the result of the
next yield .
generator.throw
As we observed in the examples above, the outer code may pass a value into the generator, as
the result of yield .
…But it can also initiate (throw) an error there. That’s natural, as an error is a kind of result.
To pass an error into a yield , we should call generator.throw(err) . In that case, the
err is thrown in the line with that yield .
alert("The execution does not reach here, because the exception is thrown above");
} catch(e) {
alert(e); // shows the error
}
}
The error, thrown into the generator at line (2) leads to an exception in line (1) with yield .
In the example above, try..catch catches it and shows it.
If we don’t catch it, then just like any exception, it “falls out” the generator into the calling code.
The current line of the calling code is the line with generator.throw , labelled as (2) . So we
can catch it here, like this:
function* generate() {
let result = yield "2 + 2 = ?"; // Error in this line
}
try {
generator.throw(new Error("The answer is not found in my database"));
} catch(e) {
alert(e); // shows the error
}
If we don’t catch the error there, then, as usual, it falls through to the outer calling code (if any)
and, if uncaught, kills the script.
Summary
●
Generators are created by generator functions function* f(…) {…} .
●
Inside generators (only) there exists a yield operator.
●
The outer code and the generator may exchange results via next/yield calls.
In modern JavaScript, generators are rarely used. But sometimes they come in handy, because
the ability of a function to exchange data with the calling code during the execution is quite
unique. And, surely, they are great for making iterable objects.
Also, in the next chapter we’ll learn async generators, which are used to read streams of
asynchronously generated data (e.g paginated fetches over a network) in for await ... of
loops.
In web-programming we often work with streamed data, so that’s another very important use
case.
✔ Tasks
Pseudo-random generator
One of them is testing. We may need random data: text, numbers, etc. to test things out well.
In JavaScript, we could use Math.random() . But if something goes wrong, we’d like to be able
to repeat the test, using exactly the same data.
For that, so called “seeded pseudo-random generators” are used. They take a “seed”, the first
value, and then generate the next ones using a formula so that the same seed yields the same
sequence, and hence the whole flow is easily reproducible. We only need to remember the seed
to repeat it.
1. 16807
2. 282475249
3. 1622650073
4. …and so on…
The task is to create a generator function pseudoRandom(seed) that takes seed and
creates the generator with this formula.
Usage example:
alert(generator.next().value); // 16807
alert(generator.next().value); // 282475249
alert(generator.next().value); // 1622650073
To solution
Let’s see a simple example first, to grasp the syntax, and then review a real-life use case.
Async iterators
Asynchronous iterators are similar to regular iterators, with a few syntactic differences.
A “regular” iterable object, as described in the chapter Iterables, looks like this:
let range = {
from: 1,
to: 5,
If necessary, please refer to the chapter about iterables for details about regular iterators.
Let’s make an iterable range object, like the one before, but now it will return values
asynchronously, one per second:
let range = {
from: 1,
to: 5,
(async () => {
})()
Async generators
As we already know, JavaScript also supports generators, and they are iterable.
Let’s recall a sequence generator from the chapter Generators. It generates a sequence of
values from start to end :
In regular generators we can’t use await . All values must come synchronously: there’s no
place for delay in for..of , it’s a synchronous construct.
But what if we need to use await in the generator body? To perform network requests, for
instance.
yield i;
}
}
(async () => {
})();
It’s indeed very simple. We add the async keyword, and the generator now can use await
inside of it, rely on promises and other async functions.
Async iterables
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
return <object with next to make range iterable>
}
}
A common practice for Symbol.iterator is to return a generator, rather than a plain object
with next as in the example before.
let range = {
from: 1,
to: 5,
If we’d like to add async actions into the generator, then we should replace Symbol.iterator
with async Symbol.asyncIterator :
let range = {
from: 1,
to: 5,
yield value;
}
}
};
(async () => {
})();
Real-life example
So far we’ve seen simple examples, to gain basic understanding. Now let’s review a real-life use
case.
There are many online services that deliver paginated data. For instance, when we need a list of
users, a request returns a pre-defined count (e.g. 100 users) – “one page”, and provides a URL
to the next page.
This pattern is very common. It’s not about users, but just about anything. For instance, GitHub
allows to retrieve commits in the same, paginated fashion:
●
We should make a request to URL in the form
https://api.github.com/repos/<repo>/commits .
●
It responds with a JSON of 30 commits, and also provides a link to the next page in the Link
header.
● Then we can use that link for the next request, to get more commits, and so on.
But we’d like to have a simpler API: an iterable object with commits, so that we could go over
them like this:
let repo = 'javascript-tutorial/en.javascript.info'; // GitHub repository to get commits from
We’d like to make a function fetchCommits(repo) that gets commits for us, making requests
whenever needed. And let it care about all pagination stuff. For us it’ll be a simple for
await..of .
while (url) {
const response = await fetch(url, { // (1)
headers: {'User-Agent': 'Our script'}, // github requires user-agent header
});
url = nextPage;
for(let commit of body) { // (4) yield commits one by one, until the page ends
yield commit;
}
}
}
1. We use the browser fetch method to download from a remote URL. It allows to supply
authorization and other headers if needed, here GitHub requires User-Agent .
2. The fetch result is parsed as JSON, that’s again a fetch -specific method.
3. We should get the next page URL from the Link header of the response. It has a special
format, so we use a regexp for that. The next page URL may look like
https://api.github.com/repositories/93253246/commits?page=2 , it’s
generated by GitHub itself.
4. Then we yield all commits received, and when they finish – the next while(url) iteration
will trigger, making one more request.
(async () => {
let count = 0;
})();
That’s just what we wanted. The internal mechanics of paginated requests is invisible from the
outside. For us it’s just an async generator that returns commits.
Summary
Regular iterators and generators work fine with the data that doesn’t take time to generate.
When we expect the data to come asynchronously, with delays, their async counterparts can be
used, and for await..of instead of for..of .
Method to provide
Symbol.iterator Symbol.asyncIterator
iterator
next() return value {value:…, done: Promise that resolves to {value:…, done:
is true/false} true/false}
next() return value {value:…, done: Promise that resolves to {value:…, done:
is true/false} true/false}
In web-development we often meet streams of data, when it flows chunk-by-chunk. For instance,
downloading or uploading a big file.
We can use async generators to process such data. It’s also noteworthy that in some
environments, such as browsers, there’s also another API called Streams, that provides special
interfaces to work with such streams, to transform the data and to pass it from one stream to
another (e.g. download from one place and immediately send elsewhere).
Modules
Modules, introduction
As our application grows bigger, we want to split it into multiple files, so called “modules”. A
module usually contains a class or a library of functions.
For a long time, JavaScript existed without a language-level module syntax. That wasn’t a
problem, because initially scripts were small and simple, so there was no need.
But eventually scripts became more and more complex, so the community invented a variety of
ways to organize code into modules, special libraries to load modules on demand.
For instance:
●
AMD – one of the most ancient module systems, initially implemented by the library
require.js .
●
CommonJS – the module system created for Node.js server.
●
UMD – one more module system, suggested as a universal one, compatible with AMD and
CommonJS.
Now all these slowly become a part of history, but we still can find them in old scripts.
The language-level module system appeared in the standard in 2015, gradually evolved since
then, and is now supported by all major browsers and in Node.js. So we’ll study it from now on.
What is a module?
Modules can load each other and use special directives export and import to interchange
functionality, call functions of one module from another one:
●
export keyword labels variables and functions that should be accessible from outside the
current module.
●
import allows to import functionality from other modules.
// sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
// main.js
import {sayHi} from './sayHi.js';
alert(sayHi); // function...
sayHi('John'); // Hello, John!
The import directive loads the module by path ./sayHi.js relative the current file and
assigns exported function sayHi to the corresponding variable.
As modules support special keywords and features, we must tell the browser that a script should
be treated as a module, by using the attribute <script type="module"> .
Like this:
https://plnkr.co/edit/rZprEnNmZyqNtNqxtijS?p=preview
The browser automatically fetches and evaluates the imported module (and its imports if
needed), and then runs the script.
<script type="module">
a = 5; // error
</script>
Module-level scope
Each module has its own top-level scope. In other words, top-level variables and functions from a
module are not seen in other scripts.
In the example below, two scripts are imported, and hello.js tries to use user variable
declared in user.js , and fails:
https://plnkr.co/edit/VGvBNE0UkzNB3dvy7Eql?p=preview
Modules are expected to export what they want to be accessible from outside and import
what they need.
So we should import user.js into hello.js and get the required functionality from it instead
of relying on global variables.
https://plnkr.co/edit/gF2JiU72jp2NjBI7bFgS?p=preview
In the browser, independent top-level scope also exists for each <script type="module"> :
<script type="module">
// The variable is only visible in this module script
let user = "John";
</script>
<script type="module">
alert(user); // Error: user is not defined
</script>
If we really need to make a window-level global variable, we can explicitly assign it to window
and access as window.user . But that’s an exception requiring a good reason.
// alert.js
alert("Module is evaluated!");
// 1.js
import `./alert.js`; // Module is evaluated!
// 2.js
import `./alert.js`; // (shows nothing)
In practice, top-level module code is mostly used for initialization, creation of internal data
structures, and if we want something to be reusable – export it.
// admin.js
export let admin = {
name: "John"
};
If this module is imported from multiple files, the module is only evaluated the first time, admin
object is created, and then passed to all further importers.
All importers get exactly the one and only admin object:
// 1.js
import {admin} from './admin.js';
admin.name = "Pete";
// 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
So, let’s reiterate – the module is executed only once. Exports are generated, and then they are
shared between importers, so if something changes the admin object, other modules will see
that.
Such behavior allows to configure modules on first import. We can setup its properties once, and
then in further imports it’s ready.
For instance, the admin.js module may provide certain functionality, but expect the credentials
to come into the admin object from outside:
// admin.js
export let admin = { };
In init.js , the first script of our app, we set admin.name . Then everyone will see it,
including calls made from inside admin.js itself:
// init.js
import {admin} from './admin.js';
admin.name = "Pete";
// other.js
import {admin, sayHi} from './admin.js';
alert(admin.name); // Pete
import.meta
The object import.meta contains the information about the current module.
Its content depends on the environment. In the browser, it contains the url of the script, or a
current webpage url if inside HTML:
<script type="module">
alert(import.meta.url); // script url (url of the html page for an inline script)
</script>
<script>
alert(this); // window
</script>
<script type="module">
alert(this); // undefined
</script>
Browser-specific features
There are also several browser-specific differences of scripts with type="module" compared
to regular ones.
You may want skip this section for now if you’re reading for the first time, or if you don’t use
JavaScript in a browser.
In other words:
●
downloading external module scripts <script type="module" src="..."> doesn’t
block HTML processing, they load in parallel with other resources.
●
module scripts wait until the HTML document is fully ready (even if they are tiny and load faster
than HTML), and then run.
●
relative order of scripts is maintained: scripts that go first in the document, execute first.
As a side-effect, module scripts always “see” the fully loaded HTML-page, including HTML
elements below them.
For instance:
<script type="module">
alert(typeof button); // object: the script can 'see' the button below
// as modules are deferred, the script runs after the whole page is loaded
</script>
<script>
alert(typeof button); // Error: button is undefined, the script can't see elements below
// regular scripts run immediately, before the rest of the page is processed
</script>
<button id="button">Button</button>
Please note: the second script actually runs before the first! So we’ll see undefined first, and
then object .
That’s because modules are deferred, so we wait for the document to be processed. The regular
script runs immediately, so we see its output first.
When using modules, we should be aware that HTML-page shows up as it loads, and JavaScript
modules run after that, so the user may see the page before the JavaScript application is ready.
Some functionality may not work yet. We should put “loading indicators”, or otherwise ensure that
the visitor won’t be confused by that.
Async works on inline scripts
For non-module scripts, async attribute only works on external scripts. Async scripts run
immediately when ready, independently of other scripts or the HTML document.
For example, the inline script below has async , so it doesn’t wait for anything.
It performs the import (fetches ./analytics.js ) and runs when ready, even if the HTML
document is not finished yet, or if other scripts are still pending.
That’s good for functionality that doesn’t depend on anything, like counters, ads, document-level
event listeners.
<!-- all dependencies are fetched (analytics.js), and the script runs -->
<!-- doesn't wait for the document or other <script> tags -->
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
External scripts
External scripts that have type="module" are different in two aspects:
<!-- the script my.js is fetched and executed only once -->
<script type="module" src="my.js"></script>
<script type="module" src="my.js"></script>
2. External scripts that are fetched from another origin (e.g. another site) require CORS
headers, as described in the chapter Fetch: Cross-Origin Requests. In other words, if a module
script is fetched from another origin, the remote server must supply a header Access-
Control-Allow-Origin allowing the fetch.
Compatibility, “nomodule”
Old browsers do not understand type="module" . Scripts of an unknown type are just ignored.
For them, it’s possible to provide a fallback using the nomodule attribute:
<script type="module">
alert("Runs in modern browsers");
</script>
<script nomodule>
alert("Modern browsers know both type=module and nomodule, so skip this")
alert("Old browsers ignore script with unknown type=module, but execute this.");
</script>
Build tools
In real-life, browser modules are rarely used in their “raw” form. Usually, we bundle them together
with a special tool such as Webpack and deploy to the production server.
One of the benefits of using bundlers – they give more control over how modules are resolved,
allowing bare modules and much more, like CSS/HTML modules.
Build tools do the following:
1. Take a “main” module, the one intended to be put in <script type="module"> in HTML.
2. Analyze its dependencies: imports and then imports of imports etc.
3. Build a single file with all modules (or multiple files, that’s tunable), replacing native import
calls with bundler functions, so that it works. “Special” module types like HTML/CSS modules
are also supported.
4. In the process, other transformations and optimizations may be applied:
●
Unreachable code removed.
●
Unused exports removed (“tree-shaking”).
●
Development-specific statements like console and debugger removed.
●
Modern, bleeding-edge JavaScript syntax may be transformed to older one with similar
functionality using Babel .
●
The resulting file is minified (spaces removed, variables replaced with shorter names, etc).
If we use bundle tools, then as scripts are bundled together into a single file (or few files),
import/export statements inside those scripts are replaced by special bundler functions. So
the resulting “bundled” script does not contain any import/export , it doesn’t require
type="module" , and we can put it into a regular script:
Summary
When we use modules, each module implements the functionality and exports it. Then we use
import to directly import it where it’s needed. The browser loads and evaluates the scripts
automatically.
In production, people often use bundlers such as Webpack to bundle modules together for
performance and other reasons.
In the next chapter we’ll see more examples of modules, and how things can be
exported/imported.
In the previous chapter we saw a simple use, now let’s explore more examples.
We can label any declaration as exported by placing export before it, be it a variable, function
or a class.
// export an array
export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// export a constant
export const MODULES_BECAME_STANDARD_YEAR = 2015;
// export a class
export class User {
constructor(name) {
this.name = name;
}
}
Most JavaScript style guides don’t recommend semicolons after function and class
declarations.
That’s why there’s no need for a semicolon at the end of export class and export
function :
// say.js
function sayHi(user) {
alert(`Hello, ${user}!`);
}
function sayBye(user) {
alert(`Bye, ${user}!`);
}
Import *
Usually, we put a list of what to import in curly braces import {...} , like this:
// main.js
import {sayHi, sayBye} from './say.js';
// main.js
import * as say from './say.js';
say.sayHi('John');
say.sayBye('John');
At first sight, “import everything” seems such a cool thing, short to write, why should we ever
explicitly list what we need to import?
1. Modern build tools (webpack and others) bundle modules together and optimize them to
speedup loading and remove unused stuff.
Let’s say, we added a 3rd-party library say.js to our project with many functions:
// say.js
export function sayHi() { ... }
export function sayBye() { ... }
export function becomeSilent() { ... }
// main.js
import {sayHi} from './say.js';
…Then the optimizer will see that and remove the other functions from the bundled code, thus
making the build smaller. That is called “tree-shaking”.
2. Explicitly listing what to import gives shorter names: sayHi() instead of say.sayHi() .
3. Explicit list of imports gives better overview of the code structure: what is used and where. It
makes code support and refactoring easier.
Import “as”
For instance, let’s import sayHi into the local variable hi for brevity, and import sayBye as
bye :
// main.js
import {sayHi as hi, sayBye as bye} from './say.js';
// say.js
...
export {sayHi as hi, sayBye as bye};
Now hi and bye are official names for outsiders, to be used in imports:
// main.js
import * as say from './say.js';
Export default
Mostly, the second approach is preferred, so that every “thing” resides in its own module.
Naturally, that requires a lot of files, as everything wants its own module, but that’s not a problem
at all. Actually, code navigation becomes easier if files are well-named and structured into folders.
Modules provide special export default (“the default export”) syntax to make the “one thing
per module” way look better.
// user.js
export default class User { // just add "default"
constructor(name) {
this.name = name;
}
}
// main.js
import User from './user.js'; // not {User}, just User
new User('John');
Imports without curly braces look nicer. A common mistake when starting to use modules is to
forget curly braces at all. So, remember, import needs curly braces for named exports and
doesn’t need them for the default one.
Technically, we may have both default and named exports in a single module, but in practice
people usually don’t mix them. A module has either named exports or the default one.
As there may be at most one default export per file, the exported entity may have no name.
Not giving a name is fine, because export default is only one per file, so import without
curly braces knows what to import.
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// user.js
export default class User {
constructor(name) {
this.name = name;
}
}
Here’s how to import the default export along with a named one:
// main.js
import {default as User, sayHi} from './user.js';
new User('John');
And, finally, if importing everything * as an object, then the default property is exactly the
default export:
// main.js
import * as user from './user.js';
…While for a default export, we always choose the name when importing:
So team members may use different names to import the same thing, and that’s not good.
Usually, to avoid that and keep the code consistent, there’s a rule that imported variables should
correspond to file names, e.g:
Still, some teams consider it a serious drawback of default exports. So they prefer to always use
named exports. Even if only a single thing is exported, it’s still exported under a name, without
default .
Re-export
“Re-export” syntax export ... from ... allows to import things and immediately export
them (possibly under another name), like this:
Imagine, we’re writing a “package”: a folder with a lot of modules, with some of the functionality
exported outside (tools like NPM allow to publish and distribute such packages), and many
modules are just “helpers”, for the internal use in other package modules.
auth/
index.js
user.js
helpers.js
tests/
login.js
providers/
github.js
facebook.js
...
We’d like to expose the package functionality via a single entry point, the “main file”
auth/index.js , to be used like this:
The idea is that outsiders, developers who use our package, should not meddle with its internal
structure, search for files inside our package folder. We export only what’s necessary in
auth/index.js and keep the rest hidden from prying eyes.
As the actual exported functionality is scattered among the package, we can import it into
auth/index.js and export from it:
// auth/index.js
The syntax export ... from ... is just a shorter notation for such import-export:
// auth/index.js
// import login/logout and immediately export them
export {login, logout} from './helpers.js';
Let’s say we have user.js , and we’d like to re-export class User from it:
// user.js
export default class User {
// ...
}
1. export User from './user.js' won’t work. What can go wrong?… But that’s a syntax
error!
To re-export the default export, we have to write export {default as User} , as in the
example above.
2. export * from './user.js' re-exports only named exports, but ignores the default
one.
If we’d like to re-export both named and the default export, then two statements are needed:
Summary
Here are all types of export that we covered in this and previous chapters.
You can check yourself by reading them and recalling what they mean:
●
Before declaration of a class/function/…:
● export [default] class/function/variable ...
●
Standalone export:
●
export {x [as y], ...} .
●
Re-export:
●
export {x [as y], ...} from "module"
●
export * from "module" (doesn’t re-export default).
●
export {default [as y]} from "module" (re-export default).
Import:
●
Named exports from module:
●
import {x [as y], ...} from "module"
●
Default export:
● import x from "module"
●
import {default as x} from "module"
●
Everything:
●
import * as obj from "module"
●
Import the module (its code runs), but do not assign it to a variable:
●
import "module"
We can put import/export statements at the top or at the bottom of a script, that doesn’t
matter.
sayHi();
// ...
In practice imports are usually at the start of the file, but that’s only for better convenience.
if (something) {
import {sayHi} from "./say.js"; // Error: import must be at top level
}
…But what if we really need to import something conditionally? Or at the right time? Like, load a
module upon request, when it’s really needed?
Dynamic imports
Export and import statements that we covered in previous chapters are called “static”. The syntax
is very simple and strict.
The module path must be a primitive string, can’t be a function call. This won’t work:
if(...) {
import ...; // Error, not allowed!
}
{
import ...; // Error, we can't put import in any block
}
That’s because import / export aim to provide a backbone for the code structure. That’s a
good thing, as code structure can be analyzed, modules can be gathered and bundled into one
file by special tools, unused exports can be removed (“tree-shaken”). That’s possible only
because the structure of imports/exports is simple and fixed.
The import(module) expression loads the module and returns a promise that resolves into a
module object that contains all its exports. It can be called from any place in the code.
import(modulePath)
.then(obj => <module object>)
.catch(err => <loading error, e.g. if no such module>)
Or, we could use let module = await import(modulePath) if inside an async function.
For instance, if we have the following module say.js :
// say.js
export function hi() {
alert(`Hello`);
}
hi();
bye();
// say.js
export default function() {
alert("Module loaded (export default)!");
}
…Then, in order to access it, we can use default property of the module object:
say();
https://plnkr.co/edit/zmyUQYZbECmhHGIJzKSd?p=preview
Please note:
Dynamic imports work in regular scripts, they don’t require script type="module" .
Please note:
Although import() looks like a function call, it’s a special syntax that just happens to use
parentheses (similar to super() ).
So we can’t copy import to a variable or use call/apply with it. That’s not a function.
Miscellaneous
Proxy and Reflect
A Proxy object wraps another object and intercepts operations, like reading/writing properties
and others, optionally handling them on its own, or transparently allowing the object to handle
them.
Proxies are used in many libraries and some browser frameworks. We’ll see many practical
applications in this chapter.
The syntax:
●
target – is an object to wrap, can be anything, including functions.
●
handler – proxy configuration: an object with “traps”, methods that intercept operations. –
e.g. get trap for reading a property of target , set trap for writing a property into
target , and so on.
For operations on proxy , if there’s a corresponding trap in handler , then it runs, and the
proxy has a chance to handle it, otherwise the operation is performed on target .
As we can see, without any traps, proxy is a transparent wrapper around target .
proxy
5
Proxy is a special “exotic object”. It doesn’t have own properties. With an empty handler it
transparently forwards operations to target .
For most operations on objects, there’s a so-called “internal method” in the JavaScript
specification that describes how it works at the lowest level. For instance [[Get]] , the internal
method to read a property, [[Set]] , the internal method to write a property, and so on. These
methods are only used in the specification, we can’t call them directly by name.
Proxy traps intercept invocations of these methods. They are listed in the Proxy specification
and in the table below.
For every internal method, there’s a trap in this table: the name of the method that we can add to
the handler parameter of new Proxy to intercept the operation:
Object.getOwnPropertyDescriptor , for..in ,
[[GetOwnProperty]] getOwnPropertyDescriptor
Object.keys/values/entries
Object.getOwnPropertyNames ,
[[OwnPropertyKeys]] ownKeys Object.getOwnPropertySymbols , for..in ,
Object/keys/values/entries
⚠ Invariants
JavaScript enforces some invariants – conditions that must be fulfilled by internal methods
and traps.
Traps can intercept these operations, but they must follow these rules.
Invariants ensure correct and consistent behavior of language features. The full invariants list
is in the specification . You probably won’t violate them if you’re not doing something weird.
Usually when one tries to get a non-existing array item, they get undefined , but we’ll wrap a
regular array into the proxy that traps reading and returns 0 if there’s no such property:
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (no such item)
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
Right now, if there’s no phrase, reading from dictionary returns undefined . But in
practice, leaving a phrase untranslated is usually better than undefined . So let’s make it return
an untranslated phrase in that case instead of undefined .
To achieve that, we’ll wrap dictionary in a proxy that intercepts reading operations:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
The proxy should totally replace the target object everywhere. No one should ever reference
the target object after it got proxied. Otherwise it’s easy to mess up.
Let’s say we want an array exclusively for numbers. If a value of another type is added, there
should be an error.
The set trap should return true if setting is successful, and false otherwise (triggers
TypeError ).
Please note: the built-in functionality of arrays is still working! Values are added by push . The
length property auto-increases when values are added. Our proxy doesn’t break anything.
We don’t have to override value-adding array methods like push and unshift , and so on, to
add checks in there, because internally they use the [[Set]] operation that’s intercepted by
the proxy.
Object.keys , for..in loop and most other methods that iterate over object properties use
[[OwnPropertyKeys]] internal method (intercepted by ownKeys trap) to get a list of
properties.
Such methods differ in details:
●
Object.getOwnPropertyNames(obj) returns non-symbol keys.
●
Object.getOwnPropertySymbols(obj) returns symbol keys.
●
Object.keys/values() returns non-symbol keys/values with enumerable flag
(property flags were explained in the chapter Property flags and descriptors).
●
for..in loops over non-symbol keys with enumerable flag, and also prototype keys.
In the example below we use ownKeys trap to make for..in loop over user , and also
Object.keys and Object.values , to skip properties starting with an underscore _ :
let user = {
name: "John",
age: 30,
_password: "***"
};
So far, it works.
Although, if we return a key that doesn’t exist in the object, Object.keys won’t list it:
let user = { };
Why? The reason is simple: Object.keys returns only properties with the enumerable flag.
To check for it, it calls the internal method [[GetOwnProperty]] for every property to get its
descriptor. And here, as there’s no property, its descriptor is empty, no enumerable flag, so it’s
skipped.
For Object.keys to return a property, we need it to either exist in the object, with the
enumerable flag, or we can intercept calls to [[GetOwnProperty]] (the trap
getOwnPropertyDescriptor does it), and return a descriptor with enumerable: true .
let user = { };
});
alert( Object.keys(user) ); // a, b, c
Let’s note once again: we only need to intercept [[GetOwnProperty]] if the property is
absent in the object.
There’s a widespread convention that properties and methods prefixed by an underscore _ are
internal. They shouldn’t be accessed from outside the object.
alert(user._password); // secret
let user = {
name: "John",
_password: "***"
};
Please note the important detail in the get trap, in the line (*) :
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
The reason is that object methods, such as user.checkPassword() , must be able to access
_password :
user = {
// ...
checkPassword(value) {
// object method must be able to read _password
return value === this._password;
}
}
A call to user.checkPassword() call gets proxied user as this (the object before dot
becomes this ), so when it tries to access this._password , the get trap activates (it
triggers on any property read) and throws an error.
So we bind the context of object methods to the original object, target , in the line (*) . Then
their future calls will use target as this , without any traps.
That solution usually works, but isn’t ideal, as a method may pass the unproxied object
somewhere else, and then we’ll get messed up: where’s the original object, and where’s the
proxied one?
Besides, an object may be proxied multiple times (multiple proxies may add different “tweaks” to
the object), and if we pass an unwrapped object to a method, there may be unexpected
consequences.
Such properties have their own issues though. In particular, they are not inherited.
let range = {
start: 1,
end: 10
};
has(target, property)
●
target – is the target object, passed as the first argument to new Proxy ,
●
property – property name
let range = {
start: 1,
end: 10
};
For example, let’s recall delay(f, ms) decorator, that we did in the chapter Decorators and
forwarding, call/apply.
In that chapter we did it without proxies. A call to delay(f, ms) returned a function that
forwards all calls to f after ms milliseconds.
function sayHi(user) {
alert(`Hello, ${user}!`);
}
As we’ve seen already, that mostly works. The wrapper function (*) performs the call after the
timeout.
But a wrapper function does not forward property read/write operations or anything else. After the
wrapping, the access is lost to properties of the original functions, such as name , length and
others:
function sayHi(user) {
alert(`Hello, ${user}!`);
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
The result is the same, but now not only calls, but all operations on the proxy are forwarded to the
original function. So sayHi.length is returned correctly after the wrapping in the line (*) .
Other traps exist: the full list is in the beginning of this chapter. Their usage pattern is similar to
the above.
Reflect
It was said previously that internal methods, such as [[Get]] , [[Set]] and others are
specification-only, they can’t be called directly.
The Reflect object makes that somewhat possible. Its methods are minimal wrappers around
the internal methods.
Here are examples of operations and Reflect calls that do the same:
… … …
For example:
alert(user.name); // John
In particular, Reflect allows us to call operators ( new , delete …) as functions
( Reflect.construct , Reflect.deleteProperty , …). That’s an interesting capability,
but here another thing is important.
In this example, both traps get and set transparently (as if they didn’t exist) forward
reading/writing operations to the object, showing a message:
let user = {
name: "John",
};
Here:
●
Reflect.get reads an object property.
●
Reflect.set writes an object property and returns true if successful, false otherwise.
That is, everything’s simple: if a trap wants to forward the call to the object, it’s enough to call
Reflect.<method> with the same arguments.
In most cases we can do the same without Reflect , for instance, reading a property
Reflect.get(target, prop, receiver) can be replaced by target[prop] . There
are important nuances though.
Proxying a getter
Let’s see an example that demonstrates why Reflect.get is better. And we’ll also see why
get/set have the fourth argument receiver , that we didn’t use before.
We have an object user with _name property and a getter for it.
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
alert(userProxy.name); // Guest
The get trap is “transparent” here, it returns the original property, and doesn’t do anything else.
That’s enough for our example.
Everything seems to be all right. But let’s make the example a little bit more complex.
After inheriting another object admin from user , we can observe the incorrect behavior:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let admin = {
__proto__: userProxy,
_name: "Admin"
};
// Expected: Admin
alert(admin.name); // outputs: Guest (?!?)
What’s the matter? Maybe we did something wrong with the inheritance?
1. When we read admin.name , as admin object doesn’t have such own property, the search
goes to its prototype.
3. When reading name property from the proxy, its get trap triggers and returns it from the
original object as target[prop] in the line (*) .
A call to target[prop] , when prop is a getter, runs its code in the context
this=target . So the result is this._name from the original object target , that is: from
user .
To fix such situations, we need receiver , the third argument of get trap. It keeps the correct
this to be passed to a getter. In our case that’s admin .
How to pass the context for a getter? For a regular function we could use call/apply , but
that’s a getter, it’s not “called”, just accessed.
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
Now receiver that keeps a reference to the correct this (that is admin ), is passed to the
getter using Reflect.get in the line (*) .
Reflect calls are named exactly the same way as traps and accept the same arguments. They
were specifically designed this way.
So, return Reflect... provides a safe no-brainer to forward the operation and make sure
we don’t forget anything related to that.
Proxy limitations
Proxies provide a unique way to alter or tweak the behavior of the existing objects at the lowest
level. Still, it’s not perfect. There are limitations.
Well, here’s the issue. After a built-in object like that gets proxied, the proxy doesn’t have these
internal slots, so built-in methods will fail.
For example:
Internally, a Map stores all data in its [[MapData]] internal slot. The proxy doesn’t have such
a slot. The built-in method Map.prototype.set method tries to access the internal
property this.[[MapData]] , but because this=proxy , can’t find it in proxy and just
fails.
proxy.set('test', 1);
alert(proxy.get('test')); // 1 (works!)
Now it works fine, because get trap binds function properties, such as map.set , to the target
object ( map ) itself.
Unlike the previous example, the value of this inside proxy.set(...) will be not proxy ,
but the original map . So when the internal implementation of set tries to access this.
[[MapData]] internal slot, it succeeds.
Private fields
The similar thing happens with private class fields.
For example, getName() method accesses the private #name property and breaks after
proxying:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
alert(user.getName()); // Error
The reason is that private fields are implemented using internal slots. JavaScript does not use
[[Get]]/[[Set]] when accessing them.
In the call getName() the value of this is the proxied user , and it doesn’t have the slot with
private fields.
Once again, the solution with binding the method makes it work:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
alert(user.getName()); // Guest
That said, the solution has drawbacks, as explained previously: it exposes the original object to
the method, potentially allowing it to be passed further and breaking other proxied functionality.
Proxy != target
The proxy and the original object are different objects. That’s natural, right?
So if we use the original object as a key, and then proxy it, then the proxy can’t be found:
alert(allUsers.has(user)); // true
alert(allUsers.has(user)); // false
As we can see, after proxying we can’t find user in the set allUsers , because the proxy is a
different object.
Proxies can intercept many operators, such as new (with construct ), in (with has ),
delete (with deleteProperty ) and so on.
But there’s no way to intercept a strict equality test for objects. An object is strictly equal to
itself only, and no other value.
So all operations and built-in classes that compare objects for equality will differentiate
between the object and the proxy. No transparent replacement here.
Revocable proxies
Let’s say we have a resource, and would like to close access to it any moment.
What we can do is to wrap it into a revocable proxy, without any traps. Such a proxy will forward
operations to object, and we can disable it at any moment.
The call returns an object with the proxy and revoke function to disable it.
Here’s an example:
let object = {
data: "Valuable data"
};
A call to revoke() removes all internal references to the target object from the proxy, so they
are no more connected. The target object can be garbage-collected after that.
We can also store revoke in a WeakMap , to be able to easily find it by a proxy object:
let object = {
data: "Valuable data"
};
revokes.set(proxy, revoke);
The benefit of such an approach is that we don’t have to carry revoke around. We can get it
from the map by proxy when needed.
We use WeakMap instead of Map here because it won’t block garbage collection. If a proxy
object becomes “unreachable” (e.g. no variable references it any more), WeakMap allows it to be
wiped from memory together with its revoke that we won’t need any more.
References
● Specification: Proxy .
●
MDN: Proxy .
Summary
Proxy is a wrapper around an object, that forwards operations on it to the object, optionally
trapping some of them.
We can trap:
●
Reading ( get ), writing ( set ), deleting ( deleteProperty ) a property (even a non-existing
one).
●
Calling a function ( apply trap).
●
The new operator ( construct trap).
● Many other operations (the full list is at the beginning of the article and in the docs ).
That allows us to create “virtual” properties and methods, implement default values, observable
objects, function decorators and so much more.
We can also wrap an object multiple times in different proxies, decorating it with various aspects
of functionality.
The Reflect API is designed to complement Proxy . For any Proxy trap, there’s a
Reflect call with same arguments. We should use those to forward calls to target objects.
✔ Tasks
Create a proxy that throws an error for an attempt to read of a non-existant property instead.
Write a function wrap(target) that takes an object target and return a proxy that adds this
functionality aspect.
let user = {
name: "John"
};
function wrap(target) {
return new Proxy(target, {
/* your code */
});
}
user = wrap(user);
alert(user.name); // John
alert(user.age); // Error: Property doesn't exist
To solution
Accessing array[-1]
In some programming languages, we can access array elements using negative indexes,
counted from the end.
Like this:
alert( array[-1] ); // 3
alert( array[-2] ); // 2
To solution
Observable
In other words, an object returned by makeObservable is just like the original one, but also
has the method observe(handler) that sets handler function to be called on any property
change.
Whenever a property changes, handler(key, value) is called with the name and value of
the property.
P.S. In this task, please only take care about writing to a property. Other operations can be
implemented in a similar way.
To solution
For example:
A string of code may be long, contain line breaks, function declarations, variables and so on.
For example:
The eval’ed code is executed in the current lexical environment, so it can see outer variables:
let a = 1;
function f() {
let a = 2;
eval('alert(a)'); // 2
}
f();
let x = 5;
eval("x = 10");
alert(x); // 10, value modified
In strict mode, eval has its own lexical environment. So functions and variables, declared inside
eval, are not visible outside:
Without use strict , eval doesn’t have its own lexical environment, so we would see x and
f outside.
Using “eval”
In modern programming eval is used very sparingly. It’s often said that “eval is evil”.
The reason is simple: long, long time ago JavaScript was a much weaker language, many things
could only be done with eval . But that time passed a decade ago.
Right now, there’s almost no reason to use eval . If someone is using it, there’s a good chance
they can replace it with a modern language construct or a JavaScript Module.
Please note that its ability to access outer variables has side-effects.
Code minifiers (tools used before JS gets to production, to compress it) rename local variables
into shorter ones (like a , b etc) to make the code smaller. That’s usually safe, but not if eval
is used, as local variables may be accessed from eval’ed code string. So minifiers don’t do that
renaming for all variables potentially visible from eval . That negatively affects code
compression ratio.
Using outer local variables inside eval is also considered a bad programming practice, as it
makes maintaining the code more difficult.
There are two ways how to be totally safe from such problems.
If eval’ed code doesn’t use outer variables, please call eval as window.eval(...) :
let x = 1;
{
let x = 5;
window.eval('alert(x)'); // 1 (global variable)
}
If eval’ed code needs local variables, change eval to new Function and pass them as
arguments:
f(5); // 5
The new Function construct is explained in the chapter The "new Function" syntax. It creates
a function from a string, also in the global scope. So it can’t see local variables. But it’s so much
clearer to pass them explicitly as arguments, like in the example above.
Summary
A call to eval(code) runs the string of code and returns the result of the last statement.
●
Rarely used in modern JavaScript, as there’s usually no need.
●
Can access outer local variables. That’s considered bad practice.
● Instead, to eval the code in the global scope, use window.eval(code) .
●
Or, if your code needs some data from the outer scope, use new Function and pass it as
arguments.
✔ Tasks
Eval-calculator
importance: 4
Create a calculator that prompts for an arithmetic expression and returns its result.
There’s no need to check the expression for correctness in this task. Just evaluate and return the
result.
Currying
Currying is an advanced technique of working with functions. It’s used not only in JavaScript,
but in other languages as well.
Let’s see an example first, to better understand what we’re talking about, and then practical
applications.
We’ll create a helper function curry(f) that performs currying for a two-argument f . In other
words, curry(f) for two-argument f(a, b) translates it into a function that runs as f(a)
(b) :
// usage
function sum(a, b) {
return a + b;
}
alert( curriedSum(1)(2) ); // 3
As you can see, the implementation is straightforward: it’s just two wrappers.
●
The result of curry(func) is a wrapper function(a) .
●
When it is called like sum(1) , the argument is saved in the Lexical Environment, and a new
wrapper is returned function(b) .
●
Then this wrapper is called with 2 as an argument, and it passes the call to the original sum .
More advanced implementations of currying, such as _.curry from lodash library, return a
wrapper that allows a function to be called both normally and partially:
function sum(a, b) {
return a + b;
}
For instance, we have the logging function log(date, importance, message) that
formats and outputs the information. In real projects such functions have many useful features
like sending logs over the network, here we’ll just use alert :
log = _.curry(log);
// use it
logNow("INFO", "message"); // [HH:mm] INFO message
Now logNow is log with fixed first argument, in other words “partially applied function” or
“partial” for short.
We can go further and make a convenience function for current debug logs:
So:
1. We didn’t lose anything after currying: log is still callable normally.
2. We can easily generate partial functions such as for today’s logs.
In case you’d like to get in details, here’s the “advanced” curry implementation for multi-argument
functions that we could use above.
function curry(func) {
Usage examples:
function sum(a, b, c) {
return a + b + c;
}
The new curry may look complicated, but it’s actually easy to understand.
The result of curry(func) call is the wrapper curried that looks like this:
For instance, let’s see what happens in the case of sum(a, b, c) . Three arguments, so
sum.length = 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.
A function that uses rest parameters, such as f(...args) , can’t be curried this way.
But most implementations of currying in JavaScript are advanced, as described: they also
keep the function callable in the multi-argument variant.
Summary
Currying allows to easily get 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) .
BigInt
⚠ A recent addition
This is a recent addition to the language.
BigInt is a special numeric type that provides support for integers of arbitrary length.
A bigint is created by appending n to the end of an integer literal or by calling the function
BigInt that creates bigints from strings, numbers etc.
Math operators
alert(1n + 2n); // 3
alert(5n / 2n); // 2
Please note: the division 5/2 returns the result rounded towards zero, without the decimal part.
All operations on bigints return bigints.
We should explicitly convert them if needed: using either BigInt() or Number() , like this:
// number to bigint
alert(bigint + BigInt(number)); // 3
// bigint to number
alert(Number(bigint) + number); // 3
The conversion of bigint to number is always silent, but if the bigint is too huge and won’t fit the
number type, then extra bits will be cut off, causing a precision loss.
The unary plus is not supported on bigints
The unary plus operator +value is a well-known way to convert a value to number.
Comparisons
Comparisons, such as < , > work with bigints and numbers just fine:
As numbers and bigints belong to different types, they can be equal == , but not strictly equal
=== :
alert( 1 == 1n ); // true
Boolean operations
if (0n) {
// never executes
}
Boolean operators, such as || , && and others also work with bigints similar to numbers:
alert( 1n || 2 ); // 1
alert( 0n || 2 ); // 2
Polyfills
Polyfilling bigints is tricky. The reason is that many JavaScript operators, such as + , - and so
on behave differently with bigints compared to regular numbers.
For example, division of bigints always returns an integer.
To emulate such behavior, a polyfill would need to replace all such operators with its functions.
But doing so is cumbersome and would cost a lot of performance.
Addition c = a + b c = JSBI.add(a, b)
Subtraction c = a - b c = JSBI.subtract(a, b)
… … …
…And then use the polyfill (Babel plugin) to convert JSBI calls to native bigints for those browsers
that support them.
In other words, this approach suggests that we write code in JSBI instead of native bigints. But
JSBI works with numbers as with bigints internally, closely following the specification, so the code
will be “bigint-ready”.
References
●
MDN docs on BigInt .
●
Specification .
Solutions
Hello, world!
Show an alert
To formulation
<!DOCTYPE html>
<html>
<body>
<script src="alert.js"></script>
</body>
</html>
alert("I'm JavaScript!");
To formulation
Variables
In the code below, each line corresponds to the item in the task list.
name = "John";
admin = name;
To formulation
That’s simple:
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.
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.
To formulation
Uppercase const?
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.
To formulation
Data types
String quotes
To formulation
Type Conversions
Type conversions
1. The addition with a string "" + 1 converts 1 to a string: "" + 1 = "1" , and
then we have "1" + 0 , the same rule is applied.
2. The subtraction - (like most math operations) only works with numbers, it converts
an empty string "" to 0 .
3. The addition with a string appends the number 5 to the string.
4. The subtraction always converts to numbers, so it makes " -9 " a number -9
(ignoring spaces around it).
5. null becomes 0 after the numeric conversion.
6. undefined becomes NaN after the numeric conversion.
7. Space characters, are trimmed off string start and end when a string is converted to
a number. Here the whole string consists of space characters, such as \t , \n and a
“regular” space between them. So, similarly to an empty string, it becomes 0 .
To formulation
Operators
● a = 2
● b = 2
● c = 2
● d = 1
let a = 1, b = 1;
To formulation
Assignment result
● a = 4 (multiplied by 2)
● x = 5 (calculated as 1 + 4)
To formulation
Comparisons
Comparisons
5 > 4 → true
"apple" > "pineapple" → false
"2" > "12" → true
undefined == null → true
undefined === null → false
null == "\n0\n" → false
null === +"\n0\n" → false
1. Obviously, true.
2. Dictionary comparison, hence false.
3. Again, dictionary comparison, first char of "2" is greater than the first char of "1" .
4. Values null and undefined equal each other only.
5. Strict equality is strict. Different types from both sides lead to false.
6. Similar to (4) , null only equals undefined .
7. Strict equality of different types.
To formulation
A simple page
JavaScript-code:
<script>
'use strict';
</body>
</html>
To formulation
Yes, it will.
Any string except an empty one (and "0" is not empty) becomes true in the logical
context.
if ("0") {
alert( 'Hello' );
}
To formulation
<!DOCTYPE html>
<html>
<body>
<script>
'use strict';
if (value == 'ECMAScript') {
alert('Right!');
} else {
alert("You don't know? ECMAScript!");
}
</script>
</body>
</html>
To formulation
if (value > 0) {
alert( 1 );
} else if (value < 0) {
alert( -1 );
} else {
alert( 0 );
}
To formulation
To formulation
To formulation
Logical operators
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.
To formulation
The answer: null , because it’s the first falsy value from the list.
To formulation
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.
To formulation
null || 3 || 4
To formulation
To formulation
To formulation
Details:
// 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' );
To formulation
if (userName == 'Admin') {
if (pass == 'TheMaster') {
alert( 'Welcome!' );
} else if (pass == '' || pass == null) {
alert( 'Canceled' );
} else {
alert( 'Wrong password' );
}
Note the vertical indents inside the if blocks. They are technically not required, but
make the code more readable.
To formulation
The answer: 1 .
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”):
let i = 3;
To formulation
The task demonstrates how postfix/prefix forms can lead to different results when used in
comparisons.
1.
From 1 to 4
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.
2.
From 1 to 5
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.
To formulation
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 .
To formulation
We use the “modulo” operator % to get the remainder and check for the evenness here.
To formulation
let i = 0;
while (i < 3) {
alert( `number ${i}!` );
i++;
}
To formulation
let num;
do {
num = prompt("Enter a number greater than 100?", 0);
} while (num <= 100 && num);
1. The check for num <= 100 – that is, the entered value is still not greater than
100 .
2. The check && num is false when num is null or a empty string. Then the
while loop stops too.
P.S. If num is null then num <= 100 is true , so without the 2nd check the loop
wouldn’t stop if the user clicks CANCEL. Both checks are required.
To formulation
let n = 10;
nextPrime:
for (let i = 2; i <= n; i++) { // for each i...
alert( i ); // a prime
}
There’s a lot of space to optimize it. For instance, we could look for the divisors from 2 to
square root of i . But anyway, if we want to be really efficient for large intervals, we need
to change the approach and rely on advanced maths and complex algorithms like
Quadratic sieve , General number field sieve etc.
To formulation
To precisely match the functionality of switch , the if must use a strict comparison
'===' .
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!' );
}
To formulation
The first two checks turn into two case . The third check is split into two cases:
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.
To formulation
Functions
Is "else" required?
No difference.
To formulation
function checkAge(age) {
return (age > 18) ? true : confirm('Did parents allow you?');
}
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.
To formulation
Function min(a, b)
A solution using if :
function min(a, b) {
if (a < b) {
return a;
} else {
return b;
}
}
function min(a, b) {
return a < b ? a : b;
}
To formulation
Function pow(x,n)
function pow(x, n) {
let result = x;
return result;
}
if (n < 1) {
alert(`Power ${n} is not supported, use a positive integer`);
} else {
alert( pow(x, n) );
}
To formulation
To formulation
Coding Style
Bad style
function pow(x, n) {
let result = 1;
return result;
}
if (n < 0) {
alert(`Power ${n} is not supported,
please enter an integer number greater than zero`);
} else {
alert( pow(x, n) );
}
To formulation
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 in the middle of a complex execution flow, then we’ll have to figure out
the data at that point. We’ll actually have to debug the test.
It would be much better to break the test into multiple it blocks with clearly written
inputs and outputs.
Like this:
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 :
To formulation
Objects
Hello, object
To formulation
Just loop over the object and return false immediately if there’s at least one
property.
function isEmpty(obj) {
for (let key in obj) {
// if the loop has started, there is a property
return false;
}
return true;
}
To formulation
Constant objects?
const user = {
name: "John"
};
// works
user.name = "Pete";
// error
user = 123;
To formulation
let salaries = {
John: 100,
Ann: 160,
Pete: 130
};
let sum = 0;
for (let key in salaries) {
sum += salaries[key];
}
alert(sum); // 390
To formulation
function multiplyNumeric(obj) {
for (let key in obj) {
if (typeof obj[key] == 'number') {
obj[key] *= 2;
}
}
}
To formulation
Error!
Try it:
let user = {
name: "John",
go: function() { alert(this.name) }
}
(user.go)() // error!
The error message in most browsers does not give us much of a clue about what went
wrong.
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.
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.
To formulation
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.
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.
To formulation
Answer: an error.
Try it:
function makeUser() {
return {
name: "John",
ref: this
};
};
That’s because rules that set this do not look at object definition. Only the moment of
call matters.
function makeUser() {
return {
name: "John",
ref() {
return this;
}
};
};
Now it works, because user.ref() is a method. And the value of this is set to the
object before dot . .
To formulation
Create a calculator
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() );
To formulation
Chaining
ladder.up().up().down().up().down().showStep(); // 1
We also can write a single call per line. For long chains it’s more readable:
ladder
.up()
.up()
.down()
.up()
.down()
.showStep(); // 1
To formulation
So they can, for instance, return the same externally defined object obj :
To formulation
Create new Calculator
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;
};
}
To formulation
function Accumulator(startingValue) {
this.value = startingValue;
this.read = function() {
this.value += +prompt('How much to add?', 0);
};
To formulation
Methods of primitives
str.test = 5; // (*)
alert(str.test);
Depending on whether you have use strict or not, the result may be:
To formulation
Numbers
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" .
To formulation
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:
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.
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?
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:
alert( Math.round(6.35 * 10) / 10); // 6.35 -> 63.5 -> 64(rounded) -> 6.4
To formulation
function readNumber() {
let num;
do {
num = prompt("Enter a number please?", 0);
} while ( !isFinite(num) );
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 .
To formulation
let i = 0;
while (i < 11) {
i += 0.2;
if (i > 9.8 && i < 10.2) alert( i );
}
Such things happen because of the precision losses when adding fractions like 0.2 .
To formulation
We need to “map” all values from the interval 0…1 into values from min to max .
The function:
alert( random(1, 5) );
alert( random(1, 5) );
alert( random(1, 5) );
To formulation
A random integer from min to max
The simplest, but wrong solution would be to generate a value from min to max and
round it:
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:
Now we can clearly see that 1 gets twice less values than 2 . And the same with 3 .
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 3.5 , thus adding the
required probabilities to the edges:
alert( randomInteger(1, 3) );
An alternative way could be to use Math.floor for a random number from min to
max+1 :
All intervals have the same length, making the final distribution uniform.
To formulation
Strings
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:
There’s a small problem though. If str is empty, then str[0] is undefined , and as
undefined doesn’t have the toUpperCase() method, we’ll get an error.
function ucFirst(str) {
if (!str) return str;
To formulation
function checkSpam(str) {
let lowerStr = str.toLowerCase();
To formulation
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.
To formulation
function extractCurrencyValue(str) {
return +str.slice(1);
}
To formulation
Arrays
Is array copied?
The result is 4 :
let fruits = ["Apples", "Pear", "Orange"];
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.
To formulation
Array operations.
To formulation
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 .
arr.push(function() {
alert( this );
})
arr[2](); // "a","b",function
The array has 3 values: initially it had two, plus the function.
To formulation
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.
function sumInput() {
while (true) {
// should we cancel?
if (value === "" || value === null || !isFinite(value)) break;
numbers.push(+value);
}
let sum = 0;
for (let number of numbers) {
sum += number;
}
return sum;
}
alert( sumInput() );
To formulation
A maximal subarray
Slow solution
The simplest way is to take every element and calculate sums of all subarrays starting
from it.
// 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.
function getMaxSubSum(arr) {
let maxSum = 0; // if we take no elements, zero will be returned
return maxSum;
}
The solution has a time complexety of O(n2) . In other words, if we increase the array
size 2 times, the algorithm will work 4 times longer.
For big arrays (1000, 10000 or more items) such algorithms can lead to a serious
sluggishness.
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:
function getMaxSubSum(arr) {
let maxSum = 0;
let 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
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 . 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.
To formulation
Array methods
function camelize(str) {
return str
.split('-') // splits 'my-long-word' into array ['my', 'long', 'word']
.map(
// capitalizes first letters of all array items except the first one
// converts ['my', 'long', 'word'] into ['my', 'Long', 'Word']
(word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1)
)
.join(''); // joins ['my', 'Long', 'Word'] into 'myLongWord'
}
To formulation
Filter range
function filterRange(arr, a, b) {
// added brackets around the expression for better readability
return arr.filter(item => (a <= item && item <= b));
}
To formulation
function filterRangeInPlace(arr, a, b) {
To formulation
alert( arr );
To formulation
We can use slice() to make a copy and run the sort on it:
function copySorted(arr) {
return arr.slice().sort();
}
To formulation
● Please note how methods are stored. They are simply added to this.methods
property.
● All tests and numeric conversions are done in the calculate method. In future it
may be extended to support more complex expressions.
function Calculator() {
this.methods = {
"-": (a, b) => a - b,
"+": (a, b) => a + b
};
this.calculate = function(str) {
To formulation
Map to names
To formulation
Map to objects
/*
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.
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:
Now fine.
To formulation
Sort users by age
function sortByAge(arr) {
arr.sort((a, b) => a.age > b.age ? 1 : -1);
}
sortByAge(arr);
To formulation
Shuffle an array
function shuffle(array) {
array.sort(() => Math.random() - 0.5);
}
That somewhat works, because Math.random() - 0.5 is a random number that may
be positive or negative, so the sorting function reorders elements randomly.
But because the sorting function is not meant to be used this way, not all permutations
have the same probability.
For instance, consider the code below. It runs shuffle 1000000 times and counts
appearances of all possible results:
function shuffle(array) {
array.sort(() => Math.random() - 0.5);
}
123: 250706
132: 124425
213: 249618
231: 124880
312: 125148
321: 125223
We can see the bias clearly: 123 and 213 appear much more often than others.
The result of the code may vary between JavaScript engines, but we can already see that
the approach is unreliable.
Why it doesn’t work? Generally speaking, sort is a “black box”: we throw an array and a
comparison function into it and expect the array to be sorted. But due to the utter
randomness of the comparison the black box goes mad, and how exactly it goes mad
depends on the concrete implementation that differs between engines.
There are other good ways to do the task. For instance, there’s a great algorithm called
Fisher-Yates shuffle . The idea is to walk the array in the reverse order and swap each
element with a random one before it:
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i
123: 166693
132: 166647
213: 166628
231: 167517
312: 166199
321: 166316
Looks good now: all permutations appear with the same probability.
To formulation
function getAverageAge(users) {
return users.reduce((prev, user) => prev + user.age, 0) / users.length;
}
To formulation
● For each item we’ll check if the resulting array already has that item.
● If it is so, then ignore, otherwise add to results.
function unique(arr) {
let result = [];
return result;
}
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.
Further in the chapter Map and Set we’ll see how to optimize it.
function unique(arr) {
return Array.from(new Set(arr));
}
To formulation
Filter anagrams
To find all anagrams, let’s split every word to letters and sort them. When letter-sorted, all
anagrams are same.
For instance:
We’ll use the letter-sorted variants as map keys to store only one value per each key:
function aclean(arr) {
let map = new Map();
return Array.from(map.values());
}
alert( aclean(arr) );
Two different words 'PAN' and 'nap' receive the same letter-sorted form 'anp' .
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.
function aclean(arr) {
let obj = {};
return Object.values(obj);
}
alert( aclean(arr) );
To formulation
Iterable keys
map.set("name", "John");
let keys = Array.from(map.keys());
keys.push("more");
To formulation
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
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, can’t get “all
read messages” from it directly. But we can do it by iterating over all messages and
filtering those that are in the set.
Like this:
// the symbolic property is only known to our code
let isRead = Symbol("isRead");
messages[0][isRead] = true;
Although symbols allow to lower the probability of problems, using WeakSet is better
from the architectural point of view.
To formulation
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
To formulation
function sumSalaries(salaries) {
let sum = 0;
for (let salary of Object.values(salaries)) {
sum += salary;
}
let salaries = {
"John": 100,
"Pete": 300,
"Mary": 250
};
To formulation
Count properties
function count(obj) {
return Object.keys(obj).length;
}
To formulation
Destructuring assignment
Destructuring assignment
let user = {
name: "John",
years: 30
};
To formulation
function topSalary(salaries) {
let max = 0;
let maxName = null;
for(const [name, salary] of Object.entries(salaries)) {
if (max < salary) {
max = salary;
maxName = name;
}
}
return maxName;
}
To formulation
Create a date
The new Date constructor uses the local time zone. So the only important thing to
remember is that months start from zero.
To formulation
Show a weekday
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:
function getWeekDay(date) {
let days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
return days[date.getDay()];
}
To formulation
European weekday
function getLocalDay(date) {
return day;
}
To formulation
…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.
dateCopy.setDate(date.getDate() - days);
return dateCopy.getDate();
}
To formulation
Let’s create a date using the next month, but pass zero as the day:
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”.
To formulation
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:
function getSecondsToday() {
let now = new Date();
alert( getSecondsToday() );
function getSecondsToday() {
let d = new Date();
return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
}
To formulation
function getSecondsToTomorrow() {
let now = new Date();
// tomorrow date
let tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1);
Alternative solution:
function getSecondsToTomorrow() {
let now = new Date();
let hour = now.getHours();
let minutes = now.getMinutes();
let seconds = now.getSeconds();
let totalSecondsToday = (hour * 60 + minutes) * 60 + seconds;
let totalSecondsInADay = 86400;
Please note that many countries have Daylight Savings Time (DST), so there may be
days with 23 or 25 hours. We may want to treat such days separately.
To formulation
To get the time from date till now – let’s substract the dates.
function formatDate(date) {
let diff = new Date() - date; // the difference in milliseconds
Alternative solution:
function formatDate(date) {
let dayOfMonth = date.getDate();
let month = date.getMonth() + 1;
let year = date.getFullYear();
let hour = date.getHours();
let minutes = date.getMinutes();
let diffMs = new Date() - date;
let diffSec = Math.round(diffMs / 1000);
let diffMin = diffSec / 60;
let diffHour = diffMin / 60;
// formatting
year = year.toString().slice(-2);
month = month < 10 ? '0' + month : month;
dayOfMonth = dayOfMonth < 10 ? '0' + dayOfMonth : dayOfMonth;
hour = hour < 10 ? '0' + hour : hour;
minutes = minutes < 10 ? '0' + minutes : minutes;
if (diffSec < 1) {
return 'right now';
} else if (diffMin < 1) {
return `${diffSec} sec. ago`
} else if (diffHour < 1) {
return `${diffMin} min. ago`
} else {
return `${dayOfMonth}.${month}.${year} ${hour}:${minutes}`
}
}
let user = {
name: "John Smith",
age: 35
};
To formulation
Exclude backreferences
let room = {
number: 23
};
let meetup = {
title: "Conference",
occupiedBy: [{name: "John"}, {name: "Alice"}],
place: room
};
room.occupiedBy = meetup;
meetup.self = meetup;
/*
{
"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 .
To formulation
function sumTo(n) {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
alert( sumTo(100) );
function sumTo(n) {
if (n == 1) return 1;
return n + sumTo(n - 1);
}
alert( sumTo(100) );
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. Some engines support the “tail call” optimization: if a recursive call is the very last
one in the function (like in sumTo above), then the outer function will not need to resume
the execution, so the engine doesn’t need to remember its execution context. That
removes the burden on memory, so counting sumTo(100000) becomes possible. But if
the JavaScript engine does not support tail call optimization (most of them don’t), there
will be an error: maximum stack size exceeded, because there’s usually a limitation on
the total stack size.
To formulation
Calculate factorial
By definition, a factorial is n! can be written as n * (n-1)! .
function factorial(n) {
return (n != 1) ? n * factorial(n - 1) : 1;
}
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:
function factorial(n) {
return n ? n * factorial(n - 1) : 1;
}
To formulation
Fibonacci numbers
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.
...
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.
fib ( 5)
fib(4) fib(3)
fib(1) fib(0)
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 .
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.
The start:
Let’s shift the variables: a,b will get fib(2),fib(3) , and c will get their sum:
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
*/
a = b; // now a = fib(3)
b = c; // now b = fib(4)
c = a + b; // c = fib(5)
…And so on until we get the needed value. That’s much faster than recursion and
involves no duplicate computations.
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 .
To formulation
Loop-based solution
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:
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 lose such ability.
Talking about good variable names, list here is the list itself. The first element of it.
And it should remain like that. That’s clear and reliable.
From the other side, the role of tmp is exclusively a list traversal, like i in the for
loop.
Recursive solution
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
function printList(list) {
if (list.next) {
printList(list.next); // do the same for the rest of the list
}
printList(list);
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.
To formulation
Using a recursion
We need to first output the rest of the list and then output the current one:
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 remember them
in an array, and then output what we remembered in the reverse order:
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;
}
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.
To formulation
Closure
Are counters independent?
So they have independent outer Lexical Environments, each one has its own count .
To formulation
Counter object
Both nested functions are created within the same outer Lexical Environment, so they
share access to the same count variable:
function Counter() {
let count = 0;
this.up = function() {
return ++count;
};
this.down = function() {
return --count;
};
}
alert( counter.up() ); // 1
alert( counter.up() ); // 2
alert( counter.down() ); // 1
To formulation
Function in if
The function sayHi is declared inside the if , so it only lives inside it. There is no
sayHi outside.
To formulation
For the second parentheses to work, the first ones must return a function.
Like this:
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
To formulation
Filter inBetween
function inBetween(a, b) {
return function(x) {
return x >= a && x <= b;
};
}
Filter inArray
function inArray(arr) {
return function(x) {
return arr.includes(x);
};
}
To formulation
Sort by field
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
To formulation
Army of functions
Let’s examine what’s done inside makeArmy , and the solution will become obvious.
1.
2.
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.
Then, later, the call to army[5]() will get the element army[5] from the array (it will
be a function) and call it.
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 ?
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 .
function makeArmy() {
return shooters;
}
army[0](); // 0
army[5](); // 5
Now it works correctly, because every time the code block in for (let i=0...)
{...} is executed, a new Lexical Environment is created for it, with the corresponding
variable i .
So, the value of i now lives a little bit closer. Not in makeArmy() Lexical Environment,
but in the Lexical Environment that corresponds the current loop iteration. That’s why now
it works.
for block
LexicalEnvironment
i: 0
makeArmy()
i: 1 outer LexicalEnvironment
i: 2 ...
i: 10
Another trick could be possible, let’s see it for better understanding of the subject:
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;
}
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.
To formulation
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 .
function makeCounter() {
let count = 0;
function counter() {
return count++;
}
return counter;
}
To formulation
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 Object to
primitive conversion, and we can provide our own method that returns the number.
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.
function f(b) {
currentSum += b;
return f(); // <-- recursive call
}
And in our case, we just return the function, without calling it:
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.
To formulation
Using setInterval :
// usage:
printNumbers(5, 10);
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. The function is
called after 1000ms the first time.
If we also want the function to run immediately, then we can add an additional call on a
separate line, like this:
function go() {
alert(current);
if (current == to) {
clearInterval(timerId);
}
current++;
}
go();
let timerId = setInterval(go, 1000);
}
printNumbers(5, 10);
To formulation
Any setTimeout will run only after the current code has finished.
let i = 0;
Spy decorator
The wrapper returned by spy(f) should store all arguments and then use f.apply to
forward the call.
function spy(func) {
function wrapper(...args) {
// using ...args instead of arguments to store "real" array in wrapper.calls
wrapper.calls.push(args);
return func.apply(this, args);
}
wrapper.calls = [];
return wrapper;
}
To formulation
Delaying decorator
The solution:
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.
return function(...args) {
let savedThis = this; // store this into an intermediate variable
setTimeout(function() {
f.apply(savedThis, args); // use it here
}, ms);
};
To formulation
Debounce decorator
return function() {
if (isCooldown) return;
f.apply(this, arguments);
isCooldown = true;
In the first call isCooldown is falsy, so the call proceeds, and the state changes to
true .
To formulation
Throttle decorator
function wrapper() {
if (isThrottled) { // (2)
savedArgs = arguments;
savedThis = this;
return;
}
isThrottled = true;
setTimeout(function() {
isThrottled = false; // (3)
if (savedArgs) {
wrapper.apply(savedThis, savedArgs);
savedArgs = savedThis = null;
}
}, ms);
}
return wrapper;
}
1. During the first call, the wrapper just runs func and sets the cooldown state
( isThrottled = true ).
2. In this state all calls are memorized in savedArgs/savedThis . Please note that
both the context and the arguments are equally important and should be memorized.
We need them simultaneously to reproduce the call.
3. After ms milliseconds pass, setTimeout triggers. The cooldown state is removed
( isThrottled = false ) and, if we had ignored calls, wrapper is executed with
the last memorized arguments and context.
The 3rd step runs not func , but wrapper , because we not only need to execute
func , but once again enter the cooldown state and setup the timeout to reset it.
To formulation
Function binding
Bound function as a method
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 .
To formulation
Second bind
function f() {
alert(this.name);
}
f(); // John
The exotic bound function object returned by f.bind(...) remembers the context
(and arguments if provided) only at creation time.
To formulation
The result of bind is another object. It does not have the test property.
To formulation
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.
//...
askPassword(() => user.loginOk(), () => user.loginFail());
It’s a bit less reliable though in more complex situations where user variable might
change after askPassword is called, but before the visitor answers and calls () =>
user.loginOk() .
To formulation
1.
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:
To formulation
Prototypal inheritance
To formulation
Searching algorithm
1.
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.
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.
To formulation
The method rabbit.eat is first found in the prototype, then executed with
this=rabbit .
To formulation
1.
The method speedy.eat is found in the prototype ( =hamster ), then executed with
this=speedy (the object before the dot).
2.
3.
4.
Then it calls push on it, adding the food into the stomach of the prototype.
Please note that such thing doesn’t happen in case of a simple assignment
this.stomach= :
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
};
Now all works fine, because this.stomach= does not perform a lookup of stomach .
The value is written directly into this object.
Also we can totally avoid the problem by making sure that each hamster has their own
stomach:
let hamster = {
stomach: [],
eat(food) {
this.stomach.push(food);
}
};
let speedy = {
__proto__: hamster,
stomach: []
};
let lazy = {
__proto__: hamster,
stomach: []
};
As a common solution, all properties that describe the state of a particular object, like
stomach above, should be written into that object. That prevents such problems.
To formulation
F.prototype
Changing "prototype"
Answers:
1.
true .
2.
false .
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.
To formulation
Create an object with the same constructor
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:
function User(name) {
this.name = name;
}
For instance:
function User(name) {
this.name = name;
}
User.prototype = {}; // (*)
At the end, we have let user2 = new Object('Pete') . The built-in Object
constructor ignores arguments, it always creates an empty object, similar to let user2
= {} , that’s what we have in user2 after all.
To formulation
Native prototypes
Function.prototype.defer = function(ms) {
setTimeout(this, ms);
};
function f() {
alert("Hello!");
}
To formulation
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 );
}
Please note: we use this in f.apply to make our decoration work for object
methods.
So if the wrapper function is called as an object method, then this is passed to the
original method f .
Function.prototype.defer = function(ms) {
let f = this;
return function(...args) {
setTimeout(() => f.apply(this, args), ms);
}
};
let user = {
name: "John",
sayHi() {
alert(this.name);
}
}
user.sayHi = user.sayHi.defer(1000);
user.sayHi();
To formulation
The method can take all enumerable keys using Object.keys and output their list.
dictionary.apple = "Apple";
dictionary.__proto__ = "test";
When we create a property using a descriptor, its flags are false by default. So in the
code above, dictionary.toString is non-enumerable.
See the the chapter Property flags and descriptors for review.
To formulation
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 :
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype.sayHi = function() {
alert( this.name );
}
rabbit.sayHi(); // Rabbit
Rabbit.prototype.sayHi(); // undefined
Object.getPrototypeOf(rabbit).sayHi(); // undefined
rabbit.__proto__.sayHi(); // undefined
To formulation
Rewrite to class
class Clock {
constructor({ template }) {
this.template = template;
}
render() {
let date = new Date();
console.log(output);
}
stop() {
clearInterval(this.timer);
}
start() {
this.render();
this.timer = setInterval(() => this.render(), 1000);
}
}
let clock = new Clock({template: 'h:m:s'});
clock.start();
To formulation
Class inheritance
class Animal {
constructor(name) {
this.name = name;
}
To formulation
Extended clock
start() {
this.render();
this.timer = setInterval(() => this.render(), this.precision);
}
};
Open the solution in a sandbox.
To formulation
The reason becomes obvious if we try to run it. An inheriting class constructor must call
super() . Otherwise "this" won’t be “defined”.
Even after the fix, there’s still important difference in "class Rabbit extends
Object" versus class Rabbit .
So Rabbit now provides access to static methods of Object via Rabbit , like this:
class Rabbit {}
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 .
Function.prototype Function.prototype
call: function call: function
bind: function bind: function
... ...
[[Prototype]] [[Prototype]]
Rabbit Object
constructor constructor
[[Prototype]]
Rabbit
constructor
To formulation
Strange instanceof
So, by the logic of instanceof , the prototype actually defines the type, not the
constructor function.
To formulation
The difference becomes obvious when we look at the code inside a function.
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.
function f() {
try {
alert('start');
return "result";
} catch (e) {
/// ...
} finally {
alert('cleanup!');
}
}
f(); // cleanup!
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 in these situations.
To formulation
To formulation
Introduction: callbacks
To formulation
Promise
Re-resolve a promise?
To formulation
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Please note that in this task resolve is called without arguments. We don’t return any
value from delay , just ensure the delay.
To formulation
To formulation
Promises chaining
promise
.then(f1)
.catch(f2);
promise
.then(f1, f2);
That’s because an error is passed down the chain, and in the second code piece there’s
no chain below f1 .
In other words, .then passes results/errors to the next .then/catch . So in the first
example, there’s a catch below, and in the second one there isn’t, so the error is
unhandled.
To formulation
Error in setTimeout
As said in the chapter, there’s an "implicit try..catch " around the function code. So all
synchronous errors are handled.
But here the error is generated not while the executor is running, but later. So the promise
can’t handle it.
To formulation
Async/await
if (response.status == 200) {
let json = await response.json(); // (3)
return json;
}
loadJson('no-such-user.json')
.catch(alert); // Error: 404 (4)
Notes:
1.
The function loadJson becomes async .
2.
3.
if (response.status == 200) {
return response.json(); // (3)
}
Then the outer code would have to await for that promise to resolve. In our case it
doesn’t matter.
4.
The error thrown from loadJson is handled by .catch . We can’t use await
loadJson(…) there, because we’re not in an async function.
To formulation
There are no tricks here. Just replace .catch with try...catch inside
demoGithubUser and add async/await where needed:
let user;
while(true) {
let name = prompt("Enter a name?", "iliakan");
try {
user = await loadJson(`https://api.github.com/users/${name}`);
break; // no error, exit loop
} catch(err) {
if (err instanceof HttpError && err.response.status == 404) {
// loop continues after the alert
alert("No such user, please reenter.");
} else {
// unknown error, rethrow
throw err;
}
}
}
demoGithubUser();
To formulation
return 10;
}
function f() {
// shows 10 after 1 second
wait().then(result => alert(result));
}
f();
To formulation
Generators
Pseudo-random generator
function* pseudoRandom(seed) {
let value = seed;
while(true) {
value = value * 16807 % 2147483647
yield value;
}
};
alert(generator.next().value); // 16807
alert(generator.next().value); // 282475249
alert(generator.next().value); // 1622650073
Please note, the same can be done with a regular function, like this:
function pseudoRandom(seed) {
let value = seed;
return function() {
value = value * 16807 % 2147483647;
return value;
}
}
alert(generator()); // 16807
alert(generator()); // 282475249
alert(generator()); // 1622650073
That also works. But then we lose ability to iterate with for..of and to use generator
composition, that may be useful elsewhere.
To formulation
let user = {
name: "John"
};
function wrap(target) {
return new Proxy(target, {
get(target, prop, receiver) {
if (prop in target) {
return Reflect.get(target, prop, receiver);
} else {
throw new ReferenceError(`Property doesn't exist: "${prop}"`)
}
}
});
}
user = wrap(user);
alert(user.name); // John
alert(user.age); // ReferenceError: Property doesn't exist
To formulation
Accessing array[-1]
alert(array[-1]); // 3
alert(array[-2]); // 2
To formulation
Observable
function makeObservable(target) {
// 1. Initialize handlers store
target[handlers] = [];
user = makeObservable(user);
user.name = "John";
To formulation
Eval-calculator
alert( eval(expr) );
To make things safe, and limit it to arithmetics only, we can check the expr using a
regular expression, so that it only may contain digits and operators.
To formulation