Javascript - Info Full Tutorial Ebook PDF
Javascript - Info Full Tutorial Ebook PDF
The JavaScript
language
Ilya Kantor
Built at July 10, 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 and arrows
● 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, 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
● Currying and partials
●
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
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?
The programs in this language are called scripts. They can be written right in a web page’s
HTML and run automatically as the page loads.
Scripts are provided and executed as plain text. They don’t need special preparation or
compilation to run.
In this aspect, JavaScript is very different from another language called Java .
Why JavaScript?
When JavaScript was created, it initially had another name: “LiveScript”. But Java was very
popular at that time, so it was decided that positioning a new language as a “younger
brother” of Java would help.
But as it evolved, JavaScript became a fully independent language with its own specification
called ECMAScript , 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”.
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.
The engine applies optimizations at each step of the process. It even watches the compiled
script as it runs, analyzes the data that flows through it, and applies optimizations to the
machine code based on that knowledge. When it’s done, scripts run quite fast.
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.
Examples of such restrictions include:
● JavaScript on a webpage may not read/write arbitrary files on the hard disk, copy them or
execute programs. It has no direct access to OS system functions.
Modern browsers allow it to work with files, but the access is limited and only provided if the
user does certain actions, like “dropping” a file into a browser window or selecting it via an
<input> tag.
There are ways to interact with camera/microphone and other devices, but they require a
user’s explicit permission. So a JavaScript-enabled page may not sneakily enable a web-
camera, observe the surroundings and send the information to the NSA .
● 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.
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.
That’s what makes JavaScript unique. That’s why it’s the most widespread tool for creating
browser interfaces.
While planning to learn a new technology, it’s beneficial to check its perspectives. So let’s move
on to the modern trends affecting it, including new languages and browser abilities.
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.
Modern tools make the transpilation very fast and transparent, actually allowing developers to
code in another language and auto-converting it “under the hood”.
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.
To read about new bleeding-edge features, that are “almost standard”, 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.
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 referrerd 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”.
Feature support
All these resources are useful in real-life development, as they contain valuable information
about language details, their support etc.
Please remember them (or this page) for the cases when you need in-depth information about a
particular feature.
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.
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 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
There’s an error in the JavaScript code on it. It’s hidden from a regular visitor’s eyes, so let’s
open developer tools to see it.
Press F12 or, if you’re on Mac, then Cmd+Opt+J .
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 ( Shift+Enter to input multi-line
commands).
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.
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.
Multi-line input
Usually, when we put a line of code into the console, and then press Enter , it executes.
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. Later on, you’ll learn about
Node.js and other platforms that use it.
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 hid 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 file (from the site root).
You can also provide a relative path from the current page. For instance, src="script.js"
would mean a file "script.js" in the current folder.
<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
We can have as many statements in our code as we want. Statements can be separated with a
semicolon.
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.
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' );
Please, don’t hesitate to comment your code.
Comments increase the overall code footprint, but that’s not a problem at all. There are many
tools which minify code before publishing to a production server. They remove comments, so
they don’t appear in the working scripts. Therefore, comments do not have negative effects on
production at all.
Later in the tutorial there will be a chapter Code quality that also explains how to write better
comments.
“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";
Looking ahead, let’s just note that "use strict" can be put at the start of most kinds of
functions instead of the whole script. Doing that enables strict mode in that function only. But
usually, people use it for the whole script.
⚠ 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.
alert("some code");
// "use strict" below is ignored--it must be at the top
"use strict";
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:
If it doesn’t, the most reliable way to ensure use strict would be to input the code into
console like this:
(function() {
'use strict';
// ...your code...
})()
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.
The statement below creates (in other words: declares or defines) 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!';
alert(message); // shows the variable content
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.
var instead of let
In older scripts, you may also find another keyword: var instead of let :
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:
let message;
message = 'Hello!';
alert(message);
When the value is changed, the old data is removed from the variable:
We can also declare two variables and copy data from one into the other.
let hello = 'Hello world!';
let message;
Functional languages
It’s interesting to note that there exist functional programming languages, like Scala
or Erlang that forbid changing variable values.
In such languages, once the value is stored “in the box”, it’s there forever. If we need to
store something else, the language forces us to create a new box (declare a new variable).
We can’t reuse the old one.
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
Examples of incorrect variable names:
Case matters
Variables named apple and AppLE are two different variables.
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.
alert(num); // 5
"use strict";
Constants
Variables declared using const are called “constants”. They cannot be changed. 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.
For instance, let’s make constants for colors in so-called “web” (hexadecimal) format:
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.
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.
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.
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. The code must be in strict mode to use let in
Chrome (V8).
● var – is an old-school variable declaration. Normally we don’t use it at all, but we’ll cover
subtle differences from let in the chapter 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
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?
To solution
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 seven basic data types in JavaScript. Here, we’ll cover them in general and in the
next chapters we’ll talk about each of them in detail.
A number
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.
We can get it as a result of division by zero:
alert( 1 / 0 ); // Infinity
●
NaN represents a computational error. It is a result of an incorrect or an undefined
mathematical operation, for instance:
The script will never stop with a fatal error (“die”). At worst, we’ll get NaN as the result.
Special numeric values formally belong to the “number” type. Of course they are not numbers in
the common sense of this word.
We’ll see more about working with numbers in the chapter Numbers.
A string
Double and single quotes are “simple” quotes. There’s 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)
In JavaScript, there is no such type. There’s only one type: string . A string may consist
of only one character or many of them.
A boolean (logical type)
The boolean type has only two values: true and false .
This type is commonly used to store yes/no values: true means “yes, correct”, and false
means “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;
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 have to mention it here
for completeness, but it’s better to study this type 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.
1. As an operator: typeof x .
2. As a function: typeof(x) .
In other words, it works with parentheses or without them. The result is the same.
The call to typeof x returns a string with the type name:
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
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.
ToString
String conversion is mostly obvious. A false becomes "false" , null becomes "null" ,
etc.
ToNumber
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 .
Addition ‘+’ concatenates strings
Almost all mathematical operations convert values to numbers. A notable exception is
addition + . If one of the added values is a string, the other one is also converted to a string.
This only happens when at least one of the arguments is a string. Otherwise, values are
converted to numbers.
ToBoolean
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) .
For instance:
Summary
The three most widely used type conversions are to string, to number, and to boolean.
ToString – Occurs when we output something. Can be performed with String(value) .
The conversion to string is usually obvious for primitive values.
ToNumber – Occurs in math operations. Can be performed with Number(value) .
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
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, we’re talking about two different operators here: the unary negation (single
operand: reverses the sign) and the binary subtraction (two operands: subtracts).
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.
alert( 2 - '1' ); // 1
alert( '6' / '2' ); // 3
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
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):
… … …
Precedence Name Sign
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
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 assignment operator "=" returns a value
An operator always returns a value. That’s obvious for most of them like addition + or
multiplication * . But the assignment operator follows this rule too.
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:
Exponentiation **
For instance:
alert( 2 ** 2 ); // 4 (2 * 2)
alert( 2 ** 3 ); // 8 (2 * 2 * 2)
alert( 2 ** 4 ); // 16 (2 * 2 * 2 * 2)
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.
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.
Is there any difference? Yes, but we can only see it if we use the returned value of ++/-- .
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.
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.
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);
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 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
But we’ll still be using the browser as our demo environment, so we should know at least a few
of its user-interface functions. In this chapter, we’ll get familiar with the browser functions
alert , prompt and confirm .
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.
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
To do that, we can use the if statement and the conditional operator ? , that’s also called a
“question mark” operator.
The if statement evaluates a condition and, if the condition’s 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( 'You are right!' );
In the example above, the condition is a simple equality check ( year == 2015 ), but it can be
much more complex.
If we want to execute more than one statement, we have to wrap our code block inside curly
braces:
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 “else” clause
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 .
For instance:
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 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:
alert( message );
It may be difficult at first to grasp what’s going on. But after a closer look, we can see that it’s
just an ordinary sequence of tests:
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.
The notation is shorter than the equivalent if statement, which appeals to some
programmers. But it is less readable.
if (company == 'Netscape') {
alert('Right!');
} else {
alert('Wrong.');
}
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!”
Demo in new window
To solution
Using if..else , write the code which gets a number via prompt and then shows in
alert :
To solution
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.
|| (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.
In other words, a chain of OR "||" returns the first truthy value or the last one if no truthy
value is found.
For instance:
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”.
1. Getting the first truthy value from a list of variables or expressions.
Imagine we have a list of variables which can either contain data or be null/undefined .
How can we find the first one with data?
We can use 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.
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)
The AND operator is represented with two ampersands && :
result = a && b;
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:
// if the first operand is truthy,
// AND returns the second operand:
alert( 1 && 0 ); // 0
alert( 1 && 5 ); // 5
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)
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
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:
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.
while (condition) {
// code
// so-called "loop body"
}
While the condition is true , the code from the loop body is executed.
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(…)
{…} .
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.
step i++ Executes after the body on each iteration but before the condition check.
body alert(i) Runs again and again while the condition is truthy.
Run begin
→ (if condition → run body and run step)
→ (if condition → run body and run step)
→ (if condition → run body and run step)
→ ...
If you are new to loops, 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:
for (;;) {
// repeats without limits
}
Please note that the two for semicolons ; must be present. Otherwise, there would be a
syntax error.
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.
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;
}
…it stops working. Code like this will give a syntax error:
This is just another reason not to use the question mark operator ? instead of if .
For example, in the code below we loop over i and j , prompting for the coordinates (i, j)
from (0,0) to (3,3) :
}
}
alert('Done!');
We need a way to stop the process if the user cancels the input.
The ordinary break after input would only break the inner loop. That’s not sufficient–labels,
come to the rescue!
The break <labelName> statement in the loop below breaks out to the label:
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
To make an “infinite” loop, usually the while(true) construct is used. Such a loop, just like
any other, can be stopped with the break directive.
If we don’t want to do anything 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
Which values get shown by the "for" loop?
importance: 4
For each loop write down which values it is going to show. Then compare with the answer.
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
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
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.
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.
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.
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 (empty in the example above) and finally the code of the
function, also named “the function body”, between curly braces.
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 );
}
function showMessage() {
let message = 'Hello, ' + userName;
alert(message);
}
The function has full access to the outer variable. It can modify it as well.
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 = :
Now if the text parameter is not passed, it will get the value "no text given"
Here "no text given" is a string, but it can be a more complex expression, which is only
evaluated and assigned if the parameter is missing. So, this is also possible:
Returning a value
A function can return a value back into the calling code as the result.
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?');
}
}
let age = prompt('How old are you?', 18);
if ( checkAge(age) ) {
alert( 'Access granted' );
} else {
alert( 'Access denied' );
}
It is possible to use return without a value. That causes the function to exit immediately.
For example:
function showMovie(age) {
if ( !checkAge(age) ) {
return;
}
In the code above, if checkAge(age) returns false , then showMovie won’t proceed to
the alert .
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))
So, it effectively becomes an empty return. We should put the value on the same line
instead.
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.
One function – one action
A function should do exactly what is suggested by its name, no more.
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.
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
●
Values passed to a function as parameters are copied to its local variables.
● A function may access outer variables. But it works only from inside out. The code outside of
the function doesn’t see its local variables.
● A function can return a value. If it doesn’t, then its result is undefined .
To make the code clean and easy to understand, it’s recommended to use mainly local
variables and parameters in the function, not outer variables.
It is always easier to understand a function which gets parameters, works with them and returns
a result than a function which gets no parameters, but modifies outer variables as a side-effect.
Function naming:
●
A name should clearly describe what the function does. When we see a function call in the
code, a good name instantly gives us an understanding what it does and returns.
● A function is an action, so function names are usually verbal.
● There exist many well-known function prefixes like create… , show… , get… , check…
and so on. Use them to hint what a function does.
Functions are the main building blocks of scripts. Now we’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('Do you have your parents permission to access this page?');
}
}
Rewrite it, to perform the same, but without if , in a single line.
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
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 ".
function sayHi() {
alert( "Hello" );
}
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.
It is a special value of course, in the sense that we can call it like sayHi() .
But it’s still a value. So we can work with it like with other kinds of values.
Please note again: there are no parentheses after sayHi . If there were, then func =
sayHi() would write the result of the call sayHi() into func , not the function sayHi
itself.
Note that we could also have used a Function Expression to declare sayHi , in the first line:
Everything would work the same. Even more obvious what’s going on, right?
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." );
}
Before we explore how we can write it in a much shorter way, let’s note that in the browser (and
on the server-side in some cases) such functions are quite popular. The major difference
between a real-life implementation and the example above is that real-life functions use more
complex ways to interact with the user than a simple confirm . In the browser, such a function
usually draws a nice-looking question window. But that’s another story.
The idea is that we pass a function and expect it to be “called back” later if necessary. In our
case, showOk becomes the callback for the “yes” answer, and showCancel for the “no”
answer.
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.
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.
For example, this works:
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.
sayHi("John"); // error!
Function Expressions are created when the execution reaches them. That would happen only in
the line (*) . Too late.
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 {
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
…But if a Function Declaration does not suit us for some reason, or we need a conditional
declaration (we’ve just seen an example), then Function Expression should be used.
Arrow functions
There’s one more very simple and concise syntax for creating functions, that’s often better than
Function Expressions. It’s called “arrow functions”, because it looks like this:
…This creates a function func that has arguments arg1..argN , evaluates the
expression on the right side with their use and returns its result.
alert( sum(1, 2) ); // 3
If we have only one argument, then parentheses can be omitted, making that even shorter:
// same as
// let double = function(n) { return n * 2 }
let double = n => n * 2;
alert( double(3) ); // 6
If there are no arguments, parentheses should be empty (but they should be present):
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.
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, use return to get results
};
alert( sum(1, 2) ); // 3
More to come
Here we praised arrow functions for brevity. But that’s not all! Arrow functions have other
interesting features. We’ll return to them later in the chapter Arrow functions revisited.
For now, we can already use them for one-line actions and callbacks.
Summary
●
Functions are values. They can be assigned, copied or declared in any place of the code.
● If the function is declared as a separate statement in the main code flow, that’s called 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.
So we should use a Function Expression only when a Function Declaration is not fit for the task.
We’ve seen a couple of examples of that in this chapter, and will see more in the future.
Arrow functions are handy for one-liners. They come in two flavors:
1. Without curly braces: (...args) => expression – the right side is an expression: the
function evaluates it and returns the result.
2. With curly braces: (...args) => { body } – brackets allow us to write multiple
statements inside the function, but we need an explicit return to return something.
✔ 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');
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.
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.
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.
Variables
let x = 5;
x = "John";
The typeof operator returns the type for a value, with two exceptions:
Interaction
We’re using a browser as a working environment, so basic UI functions will be:
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 integers on bit-level: see the docs when they are needed.
Ternary
The only operator with three parameters: cond ? resultA : resultB . If cond is truthy,
returns resultA , otherwise resultB .
Logical operators
Logical AND && and OR || perform short-circuit evaluation and then return the value where it
stopped. Logical NOT ! converts the operand to boolean type and returns the inverse value.
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:
The strict equality operator === doesn’t do the conversion: different types always mean
different values for it.
Values null and undefined are special: they equal == each other and don’t equal
anything else.
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.
The “switch” construct can replace multiple if checks. It uses === (strict equality) for
comparisons.
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;
}
Function expressions can have a name, like sum = function name(a, b) , but that
name is only visible inside that function.
3. Arrow functions:
// without arguments
let sayHi = () => alert("Hello");
● 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 .
visible in the whole code block created when the execution reaches it
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.
All modern browsers and most other environments support “debugging” – a special UI in
developer tools that makes finding and fixing errors much easier.
We’ll be using Chrome here, because it’s probably the most feature-rich in this aspect.
The “sources” pane
Your Chrome version may look a little bit different, but it still should be obvious what’s there.
●
Open the example page in Chrome.
●
Turn on developer tools with F12 (Mac: Cmd+Opt+I ).
●
Select the sources pane.
Here’s what you should see if you are doing it for the first time:
Let’s click it and select hello.js in the tree view. Here’s what should show up:
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 pane. 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 pane).
●
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, 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 is to reload the page. So let’s press F5 (Windows, Linux) or Cmd+R (Mac).
Please open the informational dropdowns to the right (labeled with arrows). They allow you to
examine the current code state:
At the current moment the debugger is inside hello() call, called by a script in
index.html (no function there, so it’s called “anonymous”).
If you click on a stack item, the debugger jumps to the corresponding code, and all its
variables can be examined as well.
Local shows local function variables. You can also see their values highlighted right over
the source.
There’s also this keyword there that we didn’t study yet, but we’ll do that soon.
There are buttons for it at the top of the right pane. Let’s engage them.
The execution has resumed, reached another breakpoint inside say() and paused there.
Take a look at the “Call stack” at the right. It has increased by one more call. We’re inside
say() now.
– make a step (run the next command), but don’t go into the function, hotkey F10 .
If we click it now, alert will be shown. The important thing is that alert can be any
function, the execution “steps over it”, skipping the function internals.
Continue to here
Right click on a line of code opens the context menu with a great option called “Continue to
here”.
That’s handy when we want to move multiple steps forward, but we’re too lazy to set a
breakpoint.
Logging
Regular users don’t see that output, it is in the console. To see it, either open the Console tab of
developer tools or press Esc while in another tab: that opens the console at the bottom.
If we have enough logging in our code, then we can see what’s going on from the records,
without the debugger.
Summary
1. A breakpoint.
2. The debugger statements.
3. An error (if dev tools are open and the button is “on”).
Then we can examine variables and step on to see where the execution goes 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 as well!
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):
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:
Line Length
No one likes to read a long horizontal line of code. It’s best practice to split them.
For example:
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 “Tab” symbol. 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:
Option 1:
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, where to put line breaks, 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.
●
Idiomatic.JS
●
StandardJS
●
(plus many more)
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.
{
"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 usually get it wrong.
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/ .
If there are many ways to solve the task, why this one? Especially when it’s not the most
obvious one.
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 only if it’s impossible to make the code so simple and self-descriptive that it doesn’t
require those.
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.
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?
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 .
A quick read of such code becomes impossible. And when there’s a typo… Ummm… We’re
stuck for long, time to drink tea.
Smart synonyms
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
Instead, reuse existing names. Just write new values into them.
An advanced variant of the approach is to covertly (!) replace the value with something
alike in the middle of a loop or a function.
For instance:
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.
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.
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.
For instance, we’re creating a function f . Wrote some code, testing: f(1) works, but f(2)
doesn’t work. We fix the code and now f(2) works. Looks complete? But we forgot to re-test
f(1) . That may lead to an error.
That’s very typical. When we develop something, we keep a lot of possible use cases in mind.
But it’s hard to expect a programmer to check all of them manually after every change. So it
becomes easy to fix one thing and break another one.
Automated testing means that tests are written separately, in addition to the code. They
can be executed automatically and check all the main use cases.
Let’s use a technique named Behavior Driven Development or, in short, BDD. That approach
is used among many projects. BDD is not just about testing. That’s more.
Let’s 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 looks like this:
describe("pow", function() {
});
A spec has three main building blocks that you can see above:
Functions assert.* are used to check whether pow works as expected. Right here we’re
using one of them – assert.equal , it compares arguments and yields an error if they are
not equal. Here it checks that the result of pow(2, 3) equals 8 .
There are other types of comparisons and checks that we’ll see further.
1. An initial spec is written, with tests for the most basic functionality.
2. An initial implementation is created.
3. To check whether it works, we run the testing framework Mocha (more details soon) that
runs the spec. While the functionality is not complete, errors are displayed. We make
corrections until everything works.
4. Now we have a working initial implementation with tests.
5. We add more use cases to the spec, probably not yet supported by the implementations.
Tests start to fail.
6. Go to 3, update the implementation till tests give no errors.
7. Repeat steps 3-6 till the functionality is ready.
So, the development is iterative. We write the spec, implement it, make sure tests pass, then
write more tests, make sure they work etc. At the end we have both a working implementation
and tests for it.
The first step is complete: we have an initial spec for pow . Now, before making the
implementaton, let’s use few JavaScript libraries to run the tests, just to see that they are
working (they will all fail).
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:
passes: 0 failures: 1 duration: 0.11s
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.
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.
If we look at the test and see two independent checks in it, it’s better to split it into two simpler
ones.
The result:
pow
✓ 2 raised to power 3 is 8 ‣
✖ 3 raised to power 3 is 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() {
// ...
it("for negative n the result is NaN", function() {
assert.isNaN(pow(2, -1));
});
});
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.
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.
1. To perform the change, no matter what. And then our users meet bugs, as we probably fail to
check something manually.
2. Or, if the punishment for errors is harsh, as there are no tests, people become afraid to
modify such functions, and then the code becomes outdated, no one wants to get into it. Not
good for development.
If the project is covered with tests, there’s just no such problem. After any changes, we can run
tests and see a lot of checks made in a matter of seconds.
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 system like webpack provide means to run
transpiler automatically on every code change, so that very easy to integrate into
development process.
A script that updates/adds new functions is called “polyfill”. It “fills in” the gap and adds
missing implementations.
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.
In contrast, objects are used to store keyed collections of various data and more complex
entities. In JavaScript, objects penetrate almost every aspect of the language. So we must
understand them first before going in-depth anywhere else.
An object can be created with figure brackets {…} with an optional list of properties. A property
is a “key: value” pair, where key is a string (also called a “property name”), and value can
be anything.
We can imagine an object as a cabinet with signed files. Every piece of data is stored in its file
by the key. It’s easy to find a file by its name or add/remove a file.
An empty object (“empty cabinet”) can be created using one of two syntaxes:
let user = new Object(); // "object constructor" syntax
let user = {}; // "object literal" syntax
Usually, the figure brackets {...} are used. That declaration is called an object literal.
We can immediately put some properties into {...} as “key: value” pairs:
A property has a key (also known as “name” or “identifier”) before the colon ":" and a value to
the right of it.
In the user object, there are two properties:
1. The first property has the name "name" and the value "John" .
2. The second one has the name "age" and the value 30 .
The resulting user object can be imagined as a cabinet with two signed files labeled “name”
and “age”.
delete user.age;
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
};
let user = {
name: "John",
age: 30,
}
That is called a “trailing” or “hanging” comma. Makes it easier to add/remove/move around
properties, because all lines become alike.
Square brackets
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:
// 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. The dot notation cannot
be used in a similar way.
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")
Computed properties
We can use square brackets in an object literal. That’s called computed properties.
For instance:
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, Set, WeakMap and WeakSet, 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:
Usually, the strict comparison "=== undefined" check works fine. But there’s a special
case when it fails, but "in" works correctly.
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!" .
A variable stores not the object itself, but its “address in memory”, in other words “a
reference” to it.
let user = {
name: "John"
};
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:
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, 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 the value of user itself. And here user stores the reference to the
same object all the time. The line (*) goes inside the object, it doesn’t reassign user .
The const would give an error if we try to set user to something else, for instance:
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.
That’s also doable, but a little bit more difficult, because there’s no built-in method for that in
JavaScript. Actually, that’s rarely needed. Copying by reference is good most of the time.
But if we really want that, then we need to create a new object and replicate the structure of the
existing one by iterating over its properties and copying them on the primitive level.
Like this:
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
};
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
Objects are associative arrays with several special features.
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
Simply put, “reachable” values are those that are accessible or usable somehow. They are
guaranteed to be stored in memory.
1. There’s a base set of inherently reachable values, that cannot be deleted for obvious
reasons.
For instance:
●
Local variables and parameters of the current function.
●
Variables and parameters for other functions on the current chain of nested calls.
●
Global variables.
●
(there are some other, internal ones as well)
These values are called roots.
2. Any other value is considered reachable if it’s reachable from a root by a reference 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
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.
user = null;
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
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
}
}
Function marry “marries” two objects by giving them references to each other and returns a
new object that contains them both.
delete family.father;
delete family.mother.husband;
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:
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.
Unreachable island
It is possible that the whole island of interlinked objects becomes unreachable and is removed
from the memory.
family = null;
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
We can clearly see an “unreachable island” to the right side. Now let’s see how “mark-and-
sweep” garbage collector deals with it.
JavaScript engines apply many optimizations to make it run faster and not affect the execution.
There are other optimizations and flavours of garbage collection algorithms. As much as I’d like
to describe them here, I have to hold off, because different engines implement different tweaks
and techniques. And, what’s even more important, things change as engines develop, so going
deeper “in advance”, without a real need is probably not worth that. Unless, of course, it is a
matter of pure interest, then there will be some links for you below.
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 only seen strings. Now let’s see the advantages 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:
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 occasionally convert one into another.
If we really want to show a symbol, we need to 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
occasionally access or overwrite.
For instance, if we’re working with user objects, that belong to a third-party code and don’t
have any id field. We’d like to add identifiers to them.
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:
// ...if later another script the uses "id" for its purposes...
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 just "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 ).
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:
alert( Symbol.keyFor(Symbol.for("name")) ); // name, global symbol
System symbols
There exist many “system” symbols that JavaScript uses internally, and we can use them to
fine-tune various aspects of our objects.
They are listed in the specification in the Well-known symbols table:
●
Symbol.hasInstance
● Symbol.isConcatSpreadable
●
Symbol.iterator
● Symbol.toPrimitive
●
…and so on.
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 occasionally 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 occasional 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!");
};
// then add as a method
user.sayHi = sayHi;
user.sayHi(); // Hello!
Object-oriented programming
When we write our code using objects to represent entities, that’s called an 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() {
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.
let user = {
name: "John",
age: 30,
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.
function sayHi() {
alert( this.name );
}
The value of this is evaluated during the run-time, depending on the context. And it can be
anything.
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.
Calling without an object: this == undefined
We can even call the function without an object at all:
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 an programming error. If there’s this inside a function, it expects to be
called in an object context.
If you come from another programming language, then you are probably used to the idea of
a "bound this ", where methods defined in an object always have this referencing that
object.
In JavaScript this is “free”, its value is evaluated at call-time and does not depend on
where the method was declared, but rather on what’s the object “before the dot”.
The concept of run-time evaluated this has both pluses and minuses. On the one hand, a
function can be reused for different objects. On the other hand, greater flexibility opens a
place for mistakes.
Here our position is not to judge whether this language design decision is good or bad. We’ll
understand how to work with it, how to get benefits and evade problems.
let user = {
name: "John",
hi() { alert(this.name); },
bye() { alert("Bye"); }
};
user.hi(); // John (the simple call works)
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() );
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.
The conversion algorithm is called ToPrimitive in the specification . It’s called with a
“hint” that specifies the conversion type.
"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. Or when an object is compared using == with a
string, number or a symbol, it’s also unclear which conversion should be done.
// binary plus
let total = car1 + car2;
// obj == string/number/symbol
if (user == 1) { ... };
The greater/less operator <> can work with both strings and numbers too. Still, it uses
“number” hint, not “default”. That’s for historical reasons.
In practice, all built-in objects except for one case ( Date object, we’ll learn it later) implement
"default" conversion the same way as "number" . And probably we should do the same.
Please note – there are only three hints. It’s that simple. There is no “boolean” hint (all objects
are true in boolean context) or anything else. And if we treat "default" and "number"
the same, like most built-ins do, then there are only two conversions.
To do the conversion, JavaScript tries to find and call three object methods:
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) {
// 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.
For instance, here user does the same as above using a combination of toString and
valueOf :
let user = {
name: "John",
money: 1000,
// for hint="string"
toString() {
return `{name: "${this.name}"}`;
},
};
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;
}
};
Return types
The important thing to know about all primitive-conversion methods is that they do not
necessarily return the “hinted” primitive.
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.
Further operations
An operation that initiated the conversion gets that primitive, and then continues to work with it,
applying further conversions if necessary.
For instance:
● Mathematical operations (except binary plus) perform ToNumber conversion:
let obj = {
toString() { // toString handles all conversions in the absence of other methods
return "2";
}
};
● Binary plus checks the primitive – if it’s a string, then it does concatenation, otherwise it
performs ToNumber and works with numbers.
String example:
let obj = {
toString() {
return "2";
}
};
let obj = {
toString() {
return true;
}
};
Summary
The specification describes explicitly which operator uses which hint. There are very few
operators that “don’t know what to expect” and use the "default" hint. Usually for built-in
objects "default" hint is handled the same way as "number" , so in practice the last two
are often merged together.
The conversion algorithm is:
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.
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";
// ...
Usually constructors don’t have a return statement. Here we mention the special behavior
with returning objects mainly for the sake of completeness.
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() { ... }
}
*/
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.
JavaScript provides constructor functions for many built-in language objects: like Date for
dates, Set for sets and others that we plan to study.
Objects, we’ll be back!
In this chapter we only cover the basics about objects and constructors. They are essential
for learning more about data types and functions in the next chapters.
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
Create new Accumulator
importance: 5
● 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. But as properties and methods are very useful in programming, JavaScript engines
try to optimize them to reduce the additional burden.
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 method str.toUpperCase() that returns a capitalized string.
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.
A number has methods of its own, for instance, toFixed(n) rounds the number to the given
precision:
let n = 1.23456;
Some languages like Java allow us to create “wrapper objects” for primitives explicitly using
a syntax like new Number(1) or new Boolean(false) .
In JavaScript, that’s also possible for historical reasons, but highly unrecommended.
Things will go crazy in several places.
For instance:
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
All numbers in JavaScript are stored in 64-bit format IEEE-754 , also known as “double
precision floating point numbers”.
Let’s recap and expand upon what we currently know about 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:
In other words, "e" multiplies the number by 1 with the given 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:
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:
For instance:
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 chart. 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”, so the number shows up
as 0.3 . But beware, the loss still 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 every number can be positive or
negative, 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:
Please note that an empty or a space-only string is treated as 0 in all numeric functions
including isFinite .
There is a special built-in method Object.is that compares values like === , but is more
reliable for two edge cases:
1. It works with NaN : Object.is(NaN, NaN) === true , that’s a good thing.
2. Values 0 and -0 are different: Object.is(0, -0) === false , technically that’s
true, because internally the number has a sign bit that may be different even if all other
bits are zeroes.
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.
There are situations when parseInt/parseFloat will return NaN . It happens when no
digits could be read:
The parseInt() function has an optional second parameter. It specifies the base of the
numeral system, so parseInt can also parse strings of hex numbers, binary numbers
and so on:
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
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
To solution
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
To solution
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
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:
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. You can read more about it in the docs . This
is called “tagged templates”. This feature makes it easier to wrap strings into custom templating
or other functionality, but it is rarely used.
Special characters
It is still possible to create multiline strings with single quotes by using a so-called “newline
character”, written as \n , which denotes a line break:
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 \n\r 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 hexadimal 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
\uXXXX
unicode 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 up to 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”.
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;
alert( `Found at ${foundPos}` );
pos = foundPos + 1; // continue the search from the next position
}
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.
For 32-bit integers the call ~n means exactly the same as -(n+1) (due to IEEE-754 format).
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.
Technically speaking, numbers are truncated to 32 bits by ~ operator, so there exist other big
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
Of the other two variants, slice is a little bit more flexible, it allows negative arguments
and shorter to write. So, it’s enough to remember solely slice of these three methods.
Comparing strings
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.
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.
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”.
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.
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
Truncate the text
importance: 5
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:
alert( fruits.length ); // 3
// mix of values
let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ];
Trailing comma
An array, just like an object, may end with a comma:
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.
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:
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.
For instance, it is copied by reference:
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
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.
The more elements in the array, the more time to move them, more in-memory
operations.
The similar thing happens with unshift : to add an element to the beginning of the array, we
need first to move existing elements to the right, increasing their indexes.
And what’s with push/pop ? They do not need to move anything. To extract an element from
the end, the pop method cleans the index and shortens length .
The 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:
Another interesting thing about the length property is that it’s writable.
If we increase it manually, nothing interesting happens. But if we decrease it, the array is
truncated. The process is irreversible, here’s the example:
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]
];
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.
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?
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.
The arr.splice(str) method is a swiss army knife for arrays. It can do everything: insert,
remove and replace elements.
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:
// 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 containing all items from index "start" to "end" (not including
"end" ). Both start and end can be negative, in that case position from array end is
assumed.
For instance:
alert( str.slice(1, 3) ); // es
alert( arr.slice(1, 3) ); // e,s
alert( str.slice(-2) ); // st
alert( arr.slice(-2) ); // s,t
concat
The method arr.concat joins the array with other arrays and/or items.
arr.concat(arg1, arg2...)
The result is a new array containing items from arr , then arg1 , arg2 etc.
For instance:
Normally, it only copies elements from arrays (“spreads” them). Other objects, even if they look
like arrays, added as a whole:
let arrayLike = {
0: "something",
length: 1
};
let arrayLike = {
0: "something",
1: "else",
[Symbol.isConcatSpreadable]: true,
length: 2
};
alert( arr.concat(arrayLike) ); // 1,2,something,else
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
alert( arr.includes(1) ); // true
Note that the methods use === comparison. So, if we look for false , it finds exactly false
and not the zero.
If we 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 :
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. 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 continues to iterate for all array elements even if true
is already returned:
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 method arr.sort sorts the array in place.
For instance:
let arr = [ 1, 2, 15 ];
Literally, all elements are converted to strings and then compared. So, the lexicographic
ordering is applied and indeed "2" > "15" .
To use our own sorting order, we need to supply a function of two arguments as the argument of
arr.sort() .
function compare(a, b) {
if (a > b) return 1;
if (a == b) return 0;
if (a < b) return -1;
}
For instance:
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
The arr.sort(fn) method has a built-in implementation of sorting algorithm. We don’t need
to care how it exactly works (an optimized quicksort 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 multiple times in the process, but it tries to make as
few comparisons as possible.
let arr = [ 1, 2, 15 ];
alert(arr); // 1, 2, 15
This works exactly the same as the other, longer, version above.
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 split method has an optional second numeric argument – a limit on the array length. If it
is provided, then the extra elements are ignored. In practice it is rarely used though:
The call arr.join(separator) does the reverse to split . It creates a string of arr items
glued by separator between them.
For instance:
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 the elements. You may notice the familiar arguments, starting from the
2nd:
●
item – is the current array item.
● index – is its position.
●
array – is the array.
alert(result); // 15
Here we used the most common variant of reduce which uses only 2 arguments.
1. On the first run, sum is the initial value (the last argument of reduce ), equals 0 , and
current is the first array element, equals 1 . So the result is 1 .
2. On the second run, sum = 1 , we add the second array element ( 2 ) to it and return.
3. On the 3rd run, sum = 3 and we add one more element to it, and so on…
As we can see, the result of the previous call becomes the first argument of the next one.
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.
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 instance, here we use an object method as a filter and thisArg comes in handy:
let user = {
age: 18,
younger(otherUser) {
return otherUser.age < this.age;
}
};
let users = [
{age: 12},
{age: 16},
{age: 32}
];
alert(youngerUsers.length); // 2
In the call above, we use user.younger as a filter and also provide user as the context for
it. If we didn’t provide the context, users.filter(user.younger) would call
user.younger as a standalone function, with this=undefined . That would mean an
instant error.
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 than it seems.
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:
let arr = [5, 3, 8, 1];
To solution
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.
Create a function copySorted(arr) that returns such a copy.
To solution
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:
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
Get average age
importance: 4
Write the function getAverageAge(users) that gets an array of objects with property age
and returns the average age.
For instance:
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 iterable also. As we’ll see, many built-in operators and methods
rely on them.
If an object represents a collection (list, set) of something, then for..of is a great syntax to
loop over it, so let’s see how to make it work.
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 .
let range = {
from: 1,
to: 5
};
To make the range iterable (and thus let for..of work) we need to add a method to the
object named Symbol.iterator (a special built-in symbol just for that).
1. When for..of starts, it calls that method once (or errors if not found). The method must
return an iterator – an object with the method next .
2. Onward, for..of works only with that returned object.
3. When for..of wants the next value, it calls next() on that object.
4. The result of next() must have the form {done: Boolean, value: any} , where
done=true means that the iteration is finished, otherwise value must be the new value.
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
Normally, internals of iterables are hidden from the external code. There’s a for..of loop,
that works, that’s all it needs to know.
But to understand things a little bit deeper let’s see how to create an iterator explicitly.
We’ll iterate over a string in exactlly the same way as for..of , but with direct calls. This code
creates a string iterator and gets values from it “manually”:
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 there all items.
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 is shorter.
alert( slice(str, 1, 3) ); // 😂𩷶
Summary
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.
Array.from(obj[, mapFn, thisArg]) makes a real Array of an iterable or array-like
obj , and we can then use array methods on it. The optional arguments mapFn and
thisArg allow us to apply a function to each item.
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:
alert( map.size ); // 3
As we can see, unlike objects, keys are not converted to strings. Any type of key is possible.
For instance:
Let’s try:
As john is an object, it got converted to the key string "[object Object]" . All objects
without a special conversion handling are converted to such string, so they’ll all mess up.
In the old times, before Map existed, people used to add unique identifiers to objects for that:
To test values for equivalence, Map uses the algorithm SameValueZero . It is roughly the
same as strict equality === , but the difference is that NaN is considered equal to NaN . So
NaN can be used as the key as well.
Chaining
Every map.set call returns the map itself, so we can “chain” the calls:
map.set('1', 'str1')
.set(1, 'num1')
.set(true, 'bool1');
Map from Object
When a Map is created, we can pass an array (or another iterable) with key-value pairs, like
this:
There is a built-in method Object.entries(obj) that returns an array of key/value pairs for an
object exactly in that format.
So we can initialize a map from an object like this:
For instance:
Set
A Set is a collection of values, where each value may occur 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.
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 again a value, 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 .
WeakSet is a special kind of Set that does not prevent JavaScript from removing its items
from memory. WeakMap is the same thing for Map .
As we know from the chapter Garbage collection, JavaScript engine stores a value in memory
while it is reachable (and can potentially be used).
For instance:
let john = { name: "John" };
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:
Or, if we use an object as the key in a regular Map , then while the Map exists, that object
exists as well. It occupies memory and may not be garbage collected.
For instance:
WeakMap/WeakSet are fundamentally different in this aspect. They do not prevent garbage-
collection of key objects.
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 is to be automatically deleted.
WeakMap does not support iteration and methods keys() , values() , entries() , so
there’s no way to get all keys or values from it.
Why such a limitation? That’s for technical reasons. If an object has lost all other references
(like john in the code above), then it is to be garbage-collected automatically. But technically
it’s not exactly specified when the cleanup happens.
The JavaScript engine decides that. It may choose to perform the memory cleanup immediately
or to wait and do the cleaning later when more deletions happen. So, technically the current
element count of a WeakMap is not known. The engine may have cleaned it up or not, or did it
partially. For that reason, methods that access WeakMap as a whole are not supported.
The idea of WeakMap is that we can store something for an object that should exist only while
the object exists. But we do not force the object to live by the mere fact that we store something
for it.
That’s useful for situations when we have a main storage for the objects somewhere and need
to keep additional information, that is only relevant while the object lives.
Let’s look at an example.
For instance, we have code that keeps a visit count for each user. The information is stored in a
map: a user is the key and the visit count is the value. When a user leaves, we don’t want to
store their visit count anymore.
One way would be to keep track of users, and when they leave – clean up the map manually:
visitsCountMap.set(john, 123);
With a regular Map , cleaning up after a user has left becomes a tedious task: we not only need
to remove the user from its main storage (be it a variable or an array), but also need to clean up
the additional stores like visitsCountMap . And it can become cumbersome in more
complex cases when users are managed in one place of the code and the additional structure is
in another place and is getting no information about removals.
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
// and when we shift our messages history, the set is cleaned up automatically
messages.shift();
The most notable limitation of WeakMap and WeakSet is the absence of iterations, and
inability to get all current content. That may appear inconvenient, but does not prevent
WeakMap/WeakSet from doing their main job – be an “additional” storage of data for objects
which are stored/managed at another place.
Summary
Regular collections:
● Map – is a collection of keyed values.
WeakMap and WeakSet are used as “secondary” data structures in addition to the “main”
object storage. Once the object is removed from the main storage, if it is only found in the
WeakMap/WeakSet , it will be cleaned up automatically.
✔ 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
From every anagram group should remain only one word, no matter which one.
To solution
Iterable keys
importance: 5
We want to get an array of map.keys() and go on working with it (apart from the map itself).
map.set("name", "John");
To solution
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
Your code can access it, but the messages are managed by someone else’s code. New
messages are added, old ones are removed regularly by that code, and you don’t know the
exact moments when it happens.
Now, which data structure you could use to store information whether the message “have been
read”? The structure must be well-suited to give the answer “was it read?” for the given
message object.
P.S. When a message is removed from messages , it should disappear from your structure as
well.
P.P.S. We shouldn’t modify message objects directly. If they are managed by someone else’s
code, then adding extra properties to them may have bad consequences.
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, once again, should disappear if the message is gone.
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.
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 order that implements its own
order.values() method. And we still can call Object.values(order) on it.
The second difference is that Object.* methods return “real” array objects, not just an
iterable. That’s mainly for historical reasons.
For instance:
let 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.
The syntax of Object.fromEntries does the reverse. Given an array of [key, value]
pairs, it creates an object:
alert(prices.orange); // 2
For example, we’d like to create a new object with double prices from the existing one.
For arrays, we have .map method that allows to transform an array, but nothing like that for
objects.
let prices = {
banana: 1,
orange: 2,
meat: 4,
};
alert(doublePrices.meat); // 8
…Or we can represent the object as an Array using Object.entries , then perform the
operations with map (and potentially other array methods), and then go back using
Object.fromEntries .
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.
E.g. we have a Map of prices, but we need to pass it to a 3rd-party code that expects an
object.
Here we go:
alert(obj.orange); // 2
✔ Tasks
Write the function sumSalaries(salaries) that returns the sum of all salaries using
Object.values and the for..of loop.
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
// 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.
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.
The code below asks for width, but not the 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
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.
In the examples above variables were declared right in the assignment: let {…} = {…} .
Of course, we could use existing variables too, without let . But there’s a catch.
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 );
}
To show JavaScript that it’s not a code block, we can make it a part of an expression by
wrapping in parentheses (...) :
// okay now
({title, width, height}) = {title: "Menu", width: 200, height: 100};
Nested destructuring
If an object or an array contain other objects and arrays, we can use more complex left-side
patterns to extract deeper portions.
In the code below options has another object in the property size and an array in the
property items . The pattern at the left side of the assignment has the same structure:
let options = {
size: {
width: 100,
height: 200
},
items: ["Cake", "Donut"],
extra: true // something extra that we will not destruct
};
alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut
The whole options object except extra that was not mentioned, is assigned to
corresponding variables.
Finally, we have width , height , item1 , item2 and title from the default value.
If we have a complex object with many properties, we can extract only what we need:
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?
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: parameterName = defaultValue
...
})
Please note that such destructuring assumes that showMenu() does have an argument. If we
want all values by default, then we should specify an empty object:
showMenu({});
We can fix this by making {} the default value for the whole destructuring thing:
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 object syntax:
Object properties that have no mapping are copied to the rest object.
●
The array syntax:
The first item goes to item1 ; the second goes into item2 , all the rest makes the array
rest .
● For more complex cases, the left side must have the same structure as the right one.
✔ 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
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.
The number of milliseconds that has passed since the beginning of 1970 is called a timestamp.
It’s a lightweight numeric representation of a date. We can always create a date from a
timestamp using new Date(timestamp) and convert the existing Date object to a
timestamp using the date.getTime() method (see below).
new Date(datestring)
If there is a single argument, and it’s a string, then it is parsed with the Date.parse algorithm
(see below).
For instance:
There are methods to access the year, month and so on from the Date object:
getFullYear()
Get the year (4 digits)
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:
// if you are in timezone UTC-1, outputs 60
// if you are in timezone UTC+3, outputs -180
alert( new Date().getTimezoneOffset() );
● setDate(date)
●
setHours(hour [, 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:
Let’s say we need to increase the date “28 Feb 2016” by 2 days. It may be “2 Mar” or “1 Mar” in
case of a leap-year. We don’t need to think about it. Just add 2 days. The Date object will do
the rest:
let date = new Date(2016, 1, 28);
date.setDate(date.getDate() + 2);
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()
If we only want to measure time, we don’t need the Date object.
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?
Such performance measurements are often called “benchmarks”.
// 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 diffSubtract(date1, date2) {
return date2 - date1;
}
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.
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:
// 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, 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:
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
Write a function getLastDayOfMonth(year, month) that returns the last day of month.
Sometimes it is 30th, 31st or even 28/29th for Feb.
Parameters:
To solution
Write a function getSecondsToday() that returns the number of seconds from the beginning
of today.
For instance, if now 10:00 am , and there was no daylight savings shift, then:
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. And, if we’re sending the object over a
network, then we also need to supply the code to “read” our object on the receiving side.
Luckily, there’s no need to write the code to handle all this. The task has been solved already.
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.
The resulting json string is called a JSON-encoded or serialized or stringified or marshalled
object. We are ready to send it over the wire or put into a plain data store.
Please note that a JSON-encoded object has several important differences from the object
literal:
● 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
};
alert( JSON.stringify(user) ); // {} (empty object)
Usually that’s fine. If that’s not what we want, then soon we’ll see how to customize the process.
The great thing is that nested objects are supported and converted automatically.
For instance:
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"]
};
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
participants are empty, because name is not in the list.
Let’s include every property except room.occupiedBy that would cause the circular
reference:
let 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.
The function will be called for every (key, value) pair and should return the “replaced”
value, which will be used instead of the original one.
In our case, we can return value “as is” for everything except occupiedBy . To ignore
occupiedBy , the code below returns undefined :
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 the whole object if necessary.
Formatting: spacer
Previously, all stringified objects had no indents and extra spaces. That’s fine if we want to send
an object over a network. The spacer argument is used exclusively for a nice output.
Here spacer = 2 tells JavaScript to show nested objects on multiple lines, with indentation
of 2 spaces inside an object:
let user = {
name: "John",
age: 25,
roles: {
isAdmin: false,
isEditor: true
}
};
The spaces parameter is used solely for logging and nice-output purposes.
Custom “toJSON”
Like toString for 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 for
the nested 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 user = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';
user = JSON.parse(user);
alert( user.friends[1] ); // 1
The JSON may be as complex as necessary, objects and arrays can include other objects and
arrays. But they must obey the format.
Here are typical mistakes in hand-written JSON (sometimes we have to write it for debugging
purposes):
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.
Whoops! An error!
The value of meetup.date is a string, not a Date object. How could JSON.parse know
that it should transform that string into a Date ?
Let’s pass to JSON.parse the reviving function that returns all values “as is”, but date will
become a Date :
let 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 there are many backreferences. And names may be used both in circular
references and normal properties.
Write replacer function to stringify everything, but remove properties that reference
meetup :
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
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.
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)
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.
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 make sure about 10000,
some engines allow more, but 100000 is probably out of limit for the majority of them. There are
automatic optimizations that help alleviate this (“tail calls optimizations”), but they are not yet
supported everywhere and work only in simple cases.
That limits the application of recursion, but it still remains very wide. There are many tasks
where recursive way of thinking gives simpler code, easier to maintain.
The execution context and stack
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.
That’s when the function starts to execute. The condition n == 1 is false, so the flow
continues into the second branch of if :
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:
Here we call the same function pow , but it absolutely doesn’t matter. The process is the same
for all functions:
●
Context: { x: 2, n: 2, at line 1 } call: pow(2, 2)
The new current execution context is on top (and bold), and previous remembered contexts are
below.
When we finish the subcall – it is easy to resume the previous context, because it keeps both
variables and the exact place of the code where it stopped. Here in the picture we use the word
“line”, but of course it’s more precise.
pow(2, 1)
The process repeats: a new subcall is made at line 5 , now with arguments x=2 , n=1 .
A new execution context is created, the previous one is pushed on top of the stack:
●
Context: { x: 2, n: 2, at line 5 } call: pow(2, 2)
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);
}
}
●
Context: { x: 2, n: 3, at line 5 } call: pow(2, 3)
The execution of pow(2, 2) is resumed. It has the result of the subcall pow(2, 1) , so it
also can finish the evaluation of x * pow(x, n - 1) , returning 4 .
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 .
function pow(x, n) {
let result = 1;
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
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? Should we stop on level 3 or make 4 levels of loops? If we put 3-4 nested subloops in
the code to traverse a single object, it becomes rather ugly.
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 (2) is the recursive step. A complex task is split into subtasks for smaller departments.
They may in turn split again, but sooner or later the split will finish at (1).
alert(sumSalaries(company)); // 6700
The code is short and easy to understand (hopefully?). That’s the power of recursion. It also
works for any level of subdepartment nesting.
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.
The natural choice would be an array:
…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:
To join:
list.next.next = secondList;
For instance, to prepend a new value, we need to update the head of the list:
To remove a value from the middle, change next of the previous one:
list.next = list.next.next;
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.
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
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
1! = 1
2! = 2 * 1 = 2
3! = 3 * 2 * 1 = 6
4! = 4 * 3 * 2 * 1 = 24
5! = 5 * 4 * 3 * 2 * 1 = 120
The task is to write a function factorial(n) that calculates n! using recursive calls.
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
}
}
}
};
To solution
Output a single-linked list from the previous task Output a single-linked list in the reverse order.
To solution
For instance:
● Math.max(arg1, arg2, ..., argN) – returns the greatest of the arguments.
● Object.assign(dest, src1, ..., srcN) – copies properties from src1..N into
dest .
●
…and so on.
In this chapter we’ll learn how to do the same. And, more importantly, how to feel comfortable
working with such functions and arrays.
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 parameters can be mentioned in a function definition with three dots ... . They
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:
There is also a special array-like object named arguments that contains all arguments by
their index.
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, no matter their total number.
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.
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.
For instance, there’s a built-in function Math.max that returns the greatest number from a
list:
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 :
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.
1. Environment Record – an object that stores all local variables as its properties (and some
other information like the value of this ).
2. A reference to the outer lexical environment, the one associated with the outer code.
So, a “variable” is just a property of the special internal object, Environment Record .
“To get or change a variable” means “to get or change a property of that object”.
For instance, in this simple code, there is only one Lexical Environment:
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 .
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
Till now, we only observed variables. Now enter Function Declarations.
Unlike let variables, they are fully initialized not when the execution reaches them, but
earlier, when a Lexical Environment is created.
For top-level functions, it means the moment when the script is started.
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 :
During the call, say() uses the outer variable phrase , let’s look at the details of what’s
going on.
First, when a function runs, a new function Lexical Environment is created automatically. That’s
a general rule for all functions. That Lexical Environment is used 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):
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 an undefined variable creates a new global variable, 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.
That’s because of the described mechanism. Old variable values are not saved anywhere.
When a function wants them, it takes the current values from its own or an outer Lexical
Environment.
function sayHi() {
alert("Hi, " + name);
}
sayHi(); // Pete
And if a function is called multiple times, then each invocation will have its own Lexical
Environment, with local variables and parameters specific for that very run.
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:
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, not to miss anything.
Environments in detail
Here’s what’s going on in the makeCounter example step-by-step, follow it to make sure that
you know things in the very 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 global Lexical Environment:
At that starting moment there is only makeCounter function, because it’s a Function
Declaration. It did not run yet.
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 for its value
makeCounter() is called. Here’s a snapshot of the moment when the execution is on the
first line inside makeCounter() :
At the moment of the call of makeCounter() , the Lexical Environment is created, to hold
its variables and arguments.
So, now we have two Lexical Environments: the first one is global, the second one is for the
current makeCounter call, with the outer reference to global.
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):
Please note that on this step the inner function was created, but not yet called. The code
inside function() { return count++; } is not running.
4. As the execution goes on, the call to makeCounter() finishes, and the result (the tiny
nested function) is assigned to the global variable counter :
That function has only one line: return count++ , that will be executed when we run it.
5. When the counter() is called, an “empty” Lexical Environment is created for it. It has no
local variables by itself. But the [[Environment]] of counter is used as the outer
reference for it, so it has access to the variables of the former makeCounter() call where
it was created:
Now if it accesses a variable, it first searches its own Lexical Environment (empty), then the
Lexical Environment of the former makeCounter() call, then the global one.
When it looks for count , it finds it among the variables makeCounter , in the nearest
outer Lexical Environment.
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.
So we return to the previous step with the only change – the new value of count . The
following calls all do the same.
The answer to the second question from the beginning of the chapter should now be obvious.
The work() function in the code below uses the 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.
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:
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 ,
then it’s also local to that Lexical Environment:
Please note: let i is visually outside of {...} . The for construct is somewhat 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 environment in JavaScript.
So programmers had to invent something. And what they did is called “immediately-invoked
function expressions” (abbreviated as IIFE).
That’s not a thing we should use nowadays, but you can find them in old scripts, so it’s better to
understand them.
(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:
So, parentheses around the function is a trick to show JavaScript that the function is created in
the context of another expression, and hence it’s a Function Expression: it needs no name and
can be called immediately.
There exist other ways besides parentheses to tell JavaScript that we mean a Function
Expression:
(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.
Garbage collection
Usually, a Lexical Environment is cleaned up and deleted after the function run. 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 its
[[Environment]] reference keeps the outer lexical environment alive as well:
function f() {
let value = 123;
return g;
}
let g = f(); // g is reachable, and keeps the outer lexical environment in memory
Please note that if f() is called many times, and resulting functions are saved, then the
corresponding Lexical Environment objects will also be retained in memory. All 3 of them in the
code below:
function f() {
let value = Math.random();
// 3 functions in array, every one of them links to Lexical Environment (LE for short)
// from the corresponding f() run
// LE LE LE
let arr = [f(), f(), f()];
A Lexical Environment object dies when it becomes unreachable (just like any other object). In
other words, it exists only while there’s at least one nested function referencing it.
In the code below, after g becomes unreachable, enclosing Lexical Environment (and hence
the value ) is cleaned from memory;
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
easy to see 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
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?
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:
/* .. your code for inBetween and inArray */
let arr = [1, 2, 3, 4, 5, 6, 7];
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
let i = 0;
while (i < 10) {
let shooter = function() { // shooter function
alert( i ); // should show its number
};
shooters.push(shooter);
i++;
}
return shooters;
}
Why all shooters show the same? Fix the code so that they work as intended.
To solution
1. let
2. const
3. var
let and const behave exactly the same way in terms of Lexical Environments.
But var is a very different beast, that originates from very old times. It’s generally not used in
modern scripts, but still lurks in the old ones.
If you don’t plan on meeting such scripts you may even skip this chapter or postpone it, but then
there’s a chance that it bites you later.
From the first sight, var behaves similar to let . That is, declares a variable:
function sayHi() {
var phrase = "Hello"; // local variable, "var" instead of "let"
alert(phrase); // Hello
}
sayHi();
alert(phrase); // Error, phrase is not defined
Variables, declared with var , are either function-wide or global. They are visible through
blocks.
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.
Declarations are hoisted, but assignments are not.
That’s better to demonstrate with an example, like this:
function sayHi() {
alert(phrase);
sayHi();
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. Mostly, the
ones 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 standartized 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.
alert("Hello");
// the same as
window.alert("Hello");
In a browser, global functions and variables declared with var 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. We’ll cover them later in the chapter
Modules.
Also, more modern variable declarations let and const do not exhibit such behavior at all:
let gLet = 5;
If a value is so important that you’d like to make it available globally, write it directly as a
property:
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 .
function sayHi() {
alert("Hi");
}
alert(sayHi.name); // sayHi
What’s more funny, the name-assigning logic is smart. It also assigns the correct name to
functions that are used in assignments:
f();
In the specification, this feature is called a “contextual name”. If the function does not provide
one, then in an assignment it is figured out from the context.
Object methods have names too:
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 provide universal handlers as well.
Custom properties
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.
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.
For instance, let’s take an ordinary Function Expression:
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.
Adding such a name also did not break anything.
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?
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.
Let’s use it to fix our code:
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.
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 (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.
But when a function is created using new Function , its [[Environment]] references not
the current Lexical Environment, but instead 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.
Our new function needs to interact with the main script.
What if it could access the outer variables?
The problem is that before JavaScript is published to production, it’s compressed using a
minifier – a special program that shrinks code by removing extra comments, spaces and –
what’s important, renames local variables into shorter ones.
For instance, if a function has let userName , minifier replaces it let a (or another letter if
this one is occupied), and does it everywhere. That’s usually a safe thing to do, because the
variable is local, nothing outside the function can access it. And inside the function, minifier
replaces every mention of it. Minifiers are smart, they analyze the code structure, so they don’t
break anything. They’re not just a dumb find-and-replace.
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 saves 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 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:
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.
Again, there is no universal specification for these methods, so that’s fine.
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 5 seconds.
Recursive setTimeout
The setTimeout above schedules the next call right at the end of the current one (*) .
The recursive 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 we 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.
Recursive setTimeout guarantees a delay between the executions, setInterval –
does not.
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.
And here is the picture for the recursive setTimeout :
That’s because a new call is planned at the end of the previous one.
Garbage collection
When a function is passed in setInterval/setTimeout , an internal reference is
created to it and saved in the scheduler. It prevents the function from being garbage
collected, even if there are no other references to it.
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 scheduler will invoke it only
after the current code is complete.
So the function is scheduled to run “right after” the current code. In other words,
asynchronously.
alert("Hello");
The first line “puts the call into calendar after 0ms”. But the scheduler will only “check the
calendar” after the current code is complete, so "Hello" is first, and "World" – after it.
There are also advanced browser-related use cases of zero-delay timeout, that we’ll discuss in
the chapter Event loop: microtasks and macrotasks.
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 setInterval(func, delay, ...args) and setTimeout(func, delay,
...args) allow to run the func regularly/once after delay milliseconds.
● To cancel the execution, we should call clearInterval/clearTimeout with the value
returned by setInterval/setTimeout .
●
Nested setTimeout calls is a more flexible alternative to setInterval . Also they can
guarantee the minimal time between the executions.
● Zero delay scheduling with setTimeout(func, 0) (the same as setTimeout(func) )
is used to schedule the call “as soon as possible, but after the current code is complete”.
● The browser limits the minimal delay for five or more nested call of setTimeout or for
setInterval (after 5th call) to 4ms. That’s for historical reasons.
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 recursive setTimeout .
To solution
Here’s the function that uses nested setTimeout to split a job into pieces.
Rewrite it to setInterval :
let i = 0;
function count() {
if (i == 1000000000) {
alert("Done in " + (Date.now() - start) + 'ms');
} else {
setTimeout(count);
}
count();
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
Transparent caching
Let’s say we have a function slow(x) which is CPU-heavy, but its results are stable. In other
words, for the same x it always returns the same result.
If the function is called often, we may want to cache (remember) the results for different x to
avoid spending extra-time on recalculations.
But instead of adding that functionality into slow() we’ll create a wrapper. As we’ll see, there
are many benefits of doing so.
Here’s the code, and explanations follow:
function slow(x) {
// there can be a heavy CPU-intensive job here
alert(`Called with ${x}`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) { // if the result is in the map
return cache.get(x); // return it
}
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 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) {
// actually, there can be a 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 .
We would observe a similar symptom if we tried to run:
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;
};
}
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
}
};
First is how to use both arguments min and max for the key in cache map. Previously, for a
single argument x we could just cache.set(x, result) to save the result and
cache.get(x) to retrieve it. But now we need to remember the result for a combination of
arguments (min,max) . The native Map takes single value only as the key.
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.
The second task to solve is how to pass many arguments to func . Currently, the wrapper
function(x) assumes a single argument, and func.call(this, x) passes it.
func.apply(context, args)
It runs the func setting this=context and using an array-like object args as the list of
arguments.
func(1, 2, 3);
func.apply(context, [1, 2, 3])
Both run func giving it arguments 1,2,3 . But apply also sets this=context .
For instance, here say is called with this=user and messageData as a list of arguments:
The only syntax difference between call and apply is that call expects a list of
arguments, while apply takes an array-like object with them.
We already know the spread operator ... from the chapter Rest parameters and spread
operator that can pass an array (or any iterable) as a list of arguments. So if we use it with
call , we can achieve almost the same as apply .
If we look more closely, there’s a minor difference between such uses of call and apply .
● The spread operator ... allows to pass iterable args as the list to call .
● The apply accepts only array-like args .
So, these calls complement each other. Where we expect an iterable, call works, where we
expect an array-like, apply works.
And if args is both iterable and array-like, like a real array, then we technically could use any
of them, but apply will probably be faster, because it’s a single operation. Most JavaScript
engines internally optimize it better than a pair call + spread .
One of the most important uses of apply is passing the call to another function, like this:
That’s called call forwarding. The wrapper passes everything it gets: the context this and
arguments to anotherFunction and returns back its result.
When an external code calls such wrapper , it is indistinguishable from the call of the original
function.
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];
}
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);
function hash() {
alert( [].join.call(arguments) ); // 1,2
}
hash(1, 2);
1. Let glue be the first argument or, if no arguments, then a comma "," .
2. Let result be an empty string.
3. Append this[0] to result .
4. Append glue and this[1] .
5. Append glue and this[2] .
6. …Do so until this.length items are glued.
7. Return result .
So, technically it takes this and joins this[0] , this[1] …etc together. It’s intentionally
written in a way that allows any array-like this (not a coincidence, many methods follow this
practice). That’s why it also works with this=arguments .
Summary
Decorator is a wrapper around a function that alters its behavior. The main job is still carried out
by the function.
It is generally safe to replace a function or a method with a decorated one, except for one little
thing. If the original function had properties on it, like func.calledCount or whatever, then
the decorated one will not provide them. Because that is a wrapper. So one needs to be careful
if one uses them. Some decorators provide their own properties.
Decorators can be seen as “features” or “aspects” that can be added to a function. We can add
one or add many. And all this without changing its code!
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
To solution
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 other future in the
closest ms milliseconds 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.
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 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).
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
update() will be called at maximum once per 100ms.
1. For the first mouse movement the decorated variant passes the call to update . That’s
important, the user sees our reaction to their move immediately.
2. Then as the mouse moves on, until 100ms nothing happens. The decorated variant
ignores calls.
3. At the end of 100ms – one more update happens with the last coordinates.
4. Then, finally, the mouse stops somewhere. The decorated variant waits until 100ms
expire and then runs update with last coordinates. So, perhaps the most important, the
final mouse coordinates are processed.
A code example:
function f(a) {
console.log(a)
};
f1000(1); // shows 1
f1000(2); // (throttling, 1000ms not out yet)
f1000(3); // (throttling, 1000ms not out yet)
P.S. Arguments and the context this passed to f1000 should be passed to the original f .
To solution
Function binding
When using setTimeout with object methods or passing object methods along, there’s a
known problem: "losing this ".
Suddenly, this just stops working right. The situation is typical for novice developers, but
happens with experienced ones as well.
Losing “this”
We already know that in JavaScript it’s easy to lose this . Once a method is passed
somewhere separately from the object – this is lost.
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 as we’ll see, usually this just becomes undefined .
The task is quite typical – we want to pass an object method somewhere else (here – to the
scheduler) where it will be called. How to make sure that it will be called in the right context?
Solution 1: a wrapper
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.
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
// ...within 1 second
user = { sayHi() { alert("Another user in setTimeout!"); } };
// Another user in setTimeout?!?
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);
}
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}!`);
}
};
If an object has many methods and we plan to actively pass it around, then we could bind
them all in a loop:
JavaScript libraries also provide functions for convenient mass binding , e.g.
_.bindAll(obj) in lodash.
Summary
✔ 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, elaborate?
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
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 first argument of every time as it’s fixed with bind .
In other cases, partial application is useful when we have a very generic function and want a
less universal variant of it for convenience.
For instance, we have a function send(from, to, text) . Then, inside a user object we
may want to use a partial variant of it: sendTo(to, text) that sends from the current user.
What if we’d like to fix some arguments, but not bind this ?
The native bind does not allow that. We can’t just omit the context and jump to arguments.
Fortunately, a partial function for binding only arguments can be easily implemented.
Like this:
// Usage:
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// add a partial method that says something now by fixing the first argument
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Hello");
// Something like:
// [10:00] John: Hello!
The result of partial(func[, arg1, arg2...]) call is a wrapper (*) that calls func
with:
● Same this as it gets (for user.sayNow call it’s user )
● Then gives it ...argsBound – arguments from the partial call ( "10:00" )
● Then gives it ...args – arguments given to the wrapper ( "Hello" )
Currying
Sometimes people mix up partial function application mentioned above with another thing
named “currying”. That’s another interesting technique of working with functions that we just
have to mention here.
Currying is a transformation of functions that translates a function from callable as f(a, b,
c) into callable as f(a)(b)(c) . In JavaScript, we usually make a wrapper to keep the
original function.
// usage
function sum(a, b) {
return a + b;
}
alert( carriedSum(1)(2) ); // 3
More advanced implementations of currying like _.curry from lodash library do something
more sophisticated. They return a wrapper that allows a function to be called normally when all
arguments are supplied or returns a partial otherwise.
function curry(f) {
return function(...args) {
// if args.length == f.length (as many arguments as f has),
// then pass the call to f
// otherwise return a partial function that fixes args as first arguments
};
}
Advanced currying allows the function to be both callable normally and partially.
For instance, we have the logging function log(date, importance, message) that
formats and outputs the information. In real projects such functions also have many other useful
features like sending logs over the network, here we just use alert :
log = _.curry(log);
After that log work both the normal way and in the curried form:
// use it
logNow("INFO", "message"); // [HH:mm] INFO message
So:
1. We didn’t lose anything after currying: log is still callable normally.
2. We were able to generate partial functions such as for today’s logs.
In case you’d like to get in details (not obligatory!), here’s the “advanced” curry implementation
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) is the wrapper curried that looks like this:
1. Call now: if passed args count is the same as the original function has in its definition
( func.length ) or longer, then just pass the call to it.
2. Get a partial: otherwise, func is not called yet. Instead, another wrapper pass is returned,
that will re-apply curried providing previous arguments together with the new ones. Then
on a new call, again, we’ll get either a new partial (if not enough arguments) or, finally, the
result.
For instance, let’s see what happens in the case of sum(a, b, c) . Three arguments, so
sum.length = 3 .
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.
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.
But most implementations of currying in JavaScript are advanced, as described: they also
keep the function callable in the multi-argument variant.
Summary
● When we fix some arguments of an existing function, the resulting (less universal) function is
called a partial. We can use bind to get a partial, but there are other ways also.
Partials are convenient when we don’t want to repeat the same argument over and over
again. Like if we have a send(from, to) function, and from should always be the same
for our task, we can get a partial and go on with it.
● Currying is a transform that makes f(a,b,c) callable as f(a)(b)(c) . JavaScript
implementations usually both keep the function callable normally and return the partial if
arguments count is not enough.
Currying is great when we want easy partials. As we’ve seen in the logging example: the
universal function log(date, importance, message) after currying gives us partials
when called with one argument like log(date) or two arguments log(date,
importance) .
✔ Tasks
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 to pass askPassword in the code below, so that it calls user.login(true) as ok
and user.login(false) as fail ?
let user = {
name: 'John',
login(result) {
alert( this.name + (result ? ' logged in' : ' failed to log in') );
}
};
askPassword(?, ?); // ?
To solution
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.
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 .
That’s great for decorators, when we need to forward a call with the current this and
arguments .
For instance, defer(f, ms) gets a function and returns a wrapper around it that delays the
call by ms milliseconds:
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. Will be in the chapter Class
inheritance).
That’s because they are meant for short pieces of code that do not have their own “context”, but
rather works in the current one. And they really shine in that use case.
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 , 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 property to work on.
descriptor
Property descriptor to apply.
If the property exists, defineProperty updates its flags. Otherwise, it creates the property
with the given value and flags; in that case, if a flag is not supplied, it is assumed false .
For instance, here a property name is created with all falsy flags:
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 .
Read-only
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 operation, but for the case when a property doesn’t exist:
let user = { };
Object.defineProperty(user, "name", {
value: "Pete",
// for new properties need to explicitly list what's true
enumerable: true,
configurable: true
});
alert(user.name); // Pete
user.name = "Alice"; // Error
Non-enumerable
let user = {
name: "John",
toString() {
return this.name;
}
};
let user = {
name: "John",
toString() {
return this.name;
}
};
Object.defineProperty(user, "toString", {
enumerable: false
});
alert(Object.keys(user)); // name
Non-configurable
Math.PI = 3; // Error
let user = { };
Object.defineProperty(user, "name", {
value: "John",
writable: false,
configurable: false
});
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
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)
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)
The first kind is data properties. We already know how to work with them. All properties that
we’ve been using till now were data properties.
The second type of properties is something new. It’s accessor properties. They are essentially
functions that work on getting and setting a value, but look like regular properties to an external
code.
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}`;
}
};
From outside, an accessor property looks like a regular one. That’s the idea of accessor
properties. We don’t call user.fullName as a function, we read it normally: the getter runs
behind the scenes.
As of now, fullName has only a getter. If we attempt to assign user.fullName= , there will
be an error.
Let’s fix it by adding a setter for user.fullName :
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
As the result, we have a “virtual” property fullName . It is readable and writable, but in fact
does not exist.
Accessor descriptors
Descriptors for accessor properties are different – as compared with data properties.
For accessor properties, there is no value and writable , but instead there are get and
set functions.
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 or a data property, 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
them.
For instance, if we want to forbid too short names for user , we can store name in a special
property _name . And filter assignments in the setter:
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
Technically, the external code may still access the name directly by using user._name . But
there is a widely known agreement that properties starting with an underscore "_" are internal
and should not be touched from outside the object.
One of the great ideas behind getters and setters – they allow to take control over a “regular”
data property at any moment by replacing it with getter and setter and tweak its behavior.
Let’s say 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 written/used by many other people. And besides, age is a nice thing to have in user ,
right? In some places it’s just what we want.
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”:
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.
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal;
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):
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
};
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
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:
That’s for data properties only, not for accessors. If a property is a getter/setter, then it behaves
like a function: getters/setters are looked up in the prototype.
For that reason admin.fullName works correctly in the code below:
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 the properties this.name and this.surname are 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 inherit from it. Then inherited objects can run its methods, and they will modify the state of
these objects, not the big one.
For instance, here animal represents a “method storage”, and rabbit makes use of it.
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)
If we had other objects like bird , snake etc inheriting from animal , they would also gain
access to methods of animal . But this in each method would be the corresponding object,
evaluated at the call-time (before dot), not animal . So when we write data into this , it is
stored into these objects.
As a result, methods are shared, but the object state is not.
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:
Note, there’s one funny thing. Where is the method rabbit.hasOwnProperty coming
from? We did not define it. Looking at the chain we can see that the method is provided by
Object.prototype.hasOwnProperty . In other words, it’s inherited.
…But why hasOwnProperty does not appear in for..in loop, like eats and jumps , if it
lists all inherited properties.
The answer is simple: it’s not enumerable. Just like all other properties of
Object.prototype , it has enumerable:false flag. That’s why they are not listed.
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 for act directly on the object, they don’t use the prototype (assuming
it’s a data property, not is a setter).
● If we call obj.method() , and the method is taken from the prototype, this still
references obj . So methods always work with the current object even if they are inherited.
● The for..in loop iterates over both own and inherited properties. All other key/value-
getting methods only operate on the object itself.
✔ Tasks
Working with prototype
importance: 5
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
We have an object:
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
Where it writes?
importance: 5
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 to fix it?
let hamster = {
stomach: [],
eat(food) {
this.stomach.push(food);
}
};
let speedy = {
__proto__: hamster
};
let lazy = {
__proto__: hamster
};
// This one found the food
speedy.eat("apple");
alert( speedy.stomach ); // apple
To solution
F.prototype
Remember, new objects can be created with a constructor function, like new F() .
If F.prototype is an object, then new operator uses it to set [[Prototype]] for the new
object.
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;
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 }
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.
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
1.
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
};
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.
We’ll see how it is for plain objects first, and then for more complex ones.
Object.prototype
Where’s the code that generates the string "[object Object]" ? That’s a built-in
toString method, but where is it? The obj is empty!
…But the short notation obj = {} is the same as obj = new Object() , where Object
is a built-in object constructor function, 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:
alert(Object.prototype.__proto__); // null
Other built-in objects such as Array , Date , Function and others also keep methods in
prototypes.
For instance, when we create an array [1, 2, 3] , the default new Array() constructor is
used internally. So the array data is written into the new object, and Array.prototype
becomes its prototype and provides methods. That’s very memory-efficient.
Some methods in prototypes may overlap, for instance, Array.prototype has its own
toString that lists comma-delimited elements:
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, then temporary
wrapper objects are created using built-in constructors String , Number , Boolean , they
provide the methods and disappear.
These objects are created invisibly to us and most engines optimize them out, but the
specification describes it exactly this way. Methods of these objects also reside in prototypes,
available as String.prototype , Number.prototype and Boolean.prototype .
⚠ Values null and undefined have no object wrappers
Special values null and undefined stand apart. They have no object wrappers, so
methods and properties are not available for them. And there are no corresponding
prototypes too.
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 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 JavaScript specification,
but not yet supported by current JavaScript engine.
Then we may 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 that the object is indeed the array.
And many built-in methods are like that.
Another possibility is to inherit by setting obj.__proto__ to Array.prototype , so all
Array methods are automatically available in obj .
But that’s impossible if obj already inherits from another object. Remember, we only can
inherit from one object at a time.
Borrowing methods is flexible, it allows to mix functionality from different objects if needed.
Summary
● All built-in objects follow the same pattern:
● The methods are stored in the prototype ( Array.prototype , Object.prototype ,
Date.prototype etc).
● The object itself stores only the data (array items, object properties, the date).
● Primitives also store methods in prototypes of wrapper objects: Number.prototype ,
String.prototype , Boolean.prototype . Only undefined and null do not have
wrapper objects.
● Built-in prototypes can be modified or populated with new methods. But it’s not
recommended to change them. Probably the only allowable cause is when we add-in a new
standard, but not yet supported by the engine JavaScript method.
✔ Tasks
Add method "f.defer(ms)" to functions
importance: 5
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).
For instance:
let animal = {
eats: true
};
alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // get the prototype of rabbit
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’s a lot! Many ways to do the
same!
Why so?
That’s for historical reasons.
●
The "prototype" property of a constructor function works since very ancient times.
● Later in the year 2012: Object.create appeared in the standard. It allowed to create
objects with the given prototype, but did not allow to get/set it. So browsers implemented
non-standard __proto__ accessor that allowed to get/set a prototype at any time.
● Later in the year 2015: Object.setPrototypeOf and Object.getPrototypeOf
were added to the standard, to perform the same functionality as __proto__ . As
__proto__ was de-facto implemented everywhere, it was kind-of deprecated and made its
way to the Annex B of the standard, that is optional for non-browser environments.
Technically, we can get/set [[Prototype]] at any time. But usually we only set it once at
the object creation time, and then do not modify: rabbit inherits from animal , and that
is not going to change.
And JavaScript engines are highly optimized to that. Changing a prototype “on-the-fly” with
Object.setPrototypeOf or obj.__proto__= is a very slow operation, it breaks
internal optimizations for object property access operations. So evade it unless you know
what you’re doing, or JavaScript speed totally doesn’t matter for you.
…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,
then the prototype may indeed be changed. As the result, the execution will go wrong in totally
unexpected ways.
What’s worst – usually developers do not think about such possibility at all. That makes such
bugs hard to notice and even turn them into vulnerabilities, especially when JavaScript is used
on server-side.
Unexpected things also may happen when accessing toString property – that’s a function
by default, and other built-in properties.
How to evade the problem?
But Object also can serve us well here, because language creators gave a thought to that
problem long ago.
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:
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 in to an
object. Just because a user may enter “proto” as the key, and there’ll be an error with hopefully
easy, but generally unpredictable consequences.
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 as other methods.
All methods that return object properties (like Object.keys and others) – return “own”
properties. If we want inherited ones, then we can use for..in .
✔ 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 new MyClass() creates 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();
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.
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 all methods, such as sayHi , in User.prototype .
Afterwards, for new objects, when we call a method, it’s taken from the prototype, just as
described in the chapter F.prototype. So a new User object has access to class methods.
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 “syntax sugar” (syntax that is designed to make things
easier to read, but doesn’t introduce anything new) in JavaScript, because we could actually
declare the same without class keyword at all:
// rewriting class User in pure functions
// Usage:
let user = new User("John");
user.sayHi();
The result of this definition is about the same. So, there are indeed reasons why class can be
considered a syntax sugar to define a constructor together with its prototype methods.
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.
Also, in addition to its basic operation, the class syntax brings many other features with it
which we’ll explore later.
Class Expression
Just like functions, classes can be defined inside another expression, passed around, returned,
assigned etc.
Here’s an example of a class expression:
Similar to Named Function Expressions, class expressions may or may not have a name.
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, generators, computed properties
etc.
Here’s an example for user.name implemented using get/set :
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 {
[f()]() {
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 is not placed into User.prototype . Instead, it is created by new , separately
for every object. So, the property will never be shared between different objects of the same
class.
Summary
class MyClass {
prop = value; // field
constructor(...) { // constructor
// ...
}
method(...) {} // method
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.
To solution
Class inheritance
Let’s say we have two classes.
Animal :
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed += speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stopped.`);
}
}
class Rabbit {
constructor(name) {
this.name = name;
}
hide() {
alert(`${this.name} hides!`);
}
}
To inherit from another class, we should specify "extends" and the parent class before the
braces {..} .
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed += speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stopped.`);
}
}
Now the Rabbit code became a bit shorter, as it uses Animal constructor by default, and it
also can run , as animals do.
As we can recall from the chapter Native prototypes, JavaScript uses the same prototypal
inheritance for build-in objects. E.g. Date.prototype.[[Prototype]] is
Object.prototype , so dates have generic object methods.
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. As of now, Rabbit inherits the stop method
that sets this.speed = 0 from Animal .
…But usually we don’t want to totally replace a parent method, but rather to build on top of it,
tweak or extend its functionality. We do something in our method, but call the parent method
before/after it or in the process.
Classes provide "super" keyword for that.
●
super.method(...) to call a parent method.
● super(...) to call a parent constructor (inside our constructor only).
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed += speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stopped.`);
}
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
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’d really understand what’s going
on.
In JavaScript, there’s a distinction between a “constructor function of an inheriting class” and all
others. In an inheriting class, the corresponding constructor function is labelled with a special
internal property [[ConstructorKind]]:"derived" .
So if we’re making a constructor of our own, then we must call super , because otherwise the
object with this reference to it won’t be created. And we’ll get an error.
For Rabbit to work, we need to call super() before using this , like here:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
class Rabbit extends Animal {
constructor(name, earLength) {
super(name);
this.earLength = earLength;
}
// ...
}
// now fine
let rabbit = new Rabbit("White Rabbit", 10);
alert(rabbit.name); // White Rabbit
alert(rabbit.earLength); // 10
Let’s get a little deeper under the hood of super . We’ll see some interesting things by the way.
First to say, from all that we’ve learned till now, it’s impossible for super to work at all!
Yeah, indeed, let’s ask ourselves, how it could technically work? When an object method runs, it
gets the current object as this . If we call super.method() then, it needs to retrieve the
method from the prototype of the current object.
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.
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.
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 !
3. …So rabbit.eat calls itself in the endless loop, because it can’t ascend any further.
[[HomeObject]]
To provide the solution, JavaScript adds one more special internal property for functions:
[[HomeObject]] .
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 animal = {
sayHi() {
console.log(`I'm an animal`);
}
};
let rabbit = {
__proto__: animal,
sayHi() {
super.sayHi();
}
};
let plant = {
sayHi() {
console.log("I'm a plant");
}
};
let tree = {
__proto__: plant,
sayHi: rabbit.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() { // should be the short syntax: eat() {...}
// ...
}
};
let rabbit = {
__proto__: animal,
eat: function() {
super.eat();
}
};
Summary
Also:
● Arrow functions don’t have 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
An example:
class User {
static staticMethod() {
alert(this === User);
}
}
User.staticMethod(); // true
class User() { }
User.staticMethod = function() {
alert(this === User);
};
The value of this inside User.staticMethod() is the class constructor User itself (the
“object before dot” rule).
Usually, static methods are used to implement functions that belong to the class, but not to any
particular object of it.
For instance, we have Article objects and need a function to compare them. The natural
choice would be Article.compare , like this:
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);
Here Article.compare stands “over” the articles, as a means to compare them. It’s not a
method of an article, but rather of the whole class.
Another example would be a so-called “factory” method. Imagine, we need few ways to create
an article:
1. Create by given parameters ( title , date etc).
2. Create an empty article with today’s date.
3. …
The first way can be implemented by the constructor. And for the second one we can make a
static method of the class.
Like Article.createTodays() here:
class Article {
constructor(title, date) {
this.title = title;
this.date = date;
}
static createTodays() {
// remember, this = Article
return new this("Today's digest", new Date());
}
}
let article = Article.createTodays();
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, just like regular class properties:
class Article {
static publisher = "Ilya Kantor";
}
class Animal {
constructor(name, speed) {
this.speed = speed;
this.name = name;
}
run(speed = 0) {
this.speed += speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
let rabbits = [
new Rabbit("White Rabbit", 10),
new Rabbit("Black Rabbit", 5)
];
rabbits.sort(Rabbit.compare);
Now we can call Rabbit.compare assuming that the inherited Animal.compare will be
called.
How does it work? Again, using prototypes. As you might have already guessed, extends
gives Rabbit the [[Prototype]] reference to Animal .
So, Rabbit function now inherits from Animal function. And Animal function normally has
[[Prototype]] referencing Function.prototype , because it doesn’t extend
anything.
class Animal {}
class Rabbit extends Animal {}
// for static properties and methods
alert(Rabbit.__proto__ === Animal); // true
Summary
Static methods are used for the functionality that doesn’t relate to a concrete class instance,
doesn’t require an instance to exist, but rather belongs to the class as a whole, like
Article.compare – a generic method to compare two articles.
Static properties are used when we’d like to store class-level data, also not bound to an
instance.
The syntax is:
class MyClass {
static property = ...;
static method() {
...
}
}
MyClass.property = ...
MyClass.method = ...
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! :)
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).
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. 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.
Protected properties are usually prefixed with an underscore _ .
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;
}
Getter/setter functions
Here we used getter/setter syntax.
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.
Protected fields are inherited
If we inherit class MegaMachine extends CoffeeMachine , then nothing prevents
us from accessing this._waterAmount or this._power from the methods of the new
class.
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:
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.
It gives the following benefits:
Protection for users, so that they don’t shoot themselves in the feet
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 to use things that are simple. At least from outside. What’s inside is a different
thing.
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. They rely on the constructor property to do so.
So when arr.filter() is called, it internally creates the new array of results using exactly
new PowerArray , 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.
So, if Rabbit extends Animal , then:
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 point to Object . So
there’s Object.keys() , but not Array.keys() and Date.keys() .
Note, there’s no link between Date and Object . Both Object and Date exist
independently. Date.prototype inherits from Object.prototype , but that’s all.
Class checking: "instanceof"
The instanceof operator allows to check whether an object belongs to a certain class. It
also takes inheritance into account.
Such a check may be necessary in many cases, here we’ll use it for building a polymorphic
function, the one that treats arguments differently depending on their type.
It returns true if obj belongs to the Class (or a class inheriting from it).
For instance:
class Rabbit {}
let rabbit = new Rabbit();
// instead of class
function Rabbit() {}
Please note that arr also belongs to the Object class. That’s because Array prototypally
inherits from Object .
The instanceof operator examines the prototype chain for the check, but we can set a
custom logic in the static method Symbol.hasInstance .
2. Most classes do not have Symbol.hasInstance . In that case, the standard logic is used:
obj instanceOf Classs checks whether Class.prototype equals to one of
prototypes in the obj prototype chain.
class Animal {}
class Rabbit extends Animal {}
That’s funny, but the Class constructor itself does not participate in the check! Only the chain
of prototypes and Class.prototype matters.
Like here:
function Rabbit() {}
let rabbit = new Rabbit();
That’s one of the reasons to avoid changing prototype . Just to keep safe.
We already know that plain objects are converted to string as [object Object] :
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"
};
As you can see, the result is exactly Symbol.toStringTag (if exists), wrapped into
[object ...] .
At the end we have “typeof on steroids” that not only works for primitive data types, but also for
built-in objects and even can be customized.
It can be used instead of instanceof for built-in objects when we want to get the type as a
string rather than just to check.
Summary
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
Why instanceof below returns true ? We can easily see that a is not created by B() .
function A() {}
function B() {}
Mixins
In JavaScript we can only inherit from a single object. There can be only one
[[Prototype]] for an object. And a class may extend only one other class.
But sometimes that feels limiting. For instance, I have a class StreetSweeper and a class
Bicycle , and want to make a StreetSweepingBicycle .
Or, talking about programming, we have a class 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.
In other words, a mixin provides methods that implement a certain behavior, but we do not use
it alone, we use it to add the behavior to other classes.
A mixin example
The simplest way to make a mixin in JavaScript is to make an object with useful methods, so
that we can easily merge them into a prototype of any class.
For instance here the mixin sayHiMixin is used to add some “speech” for User :
// mixin
let sayHiMixin = {
sayHi() {
alert(`Hello ${this.name}`);
},
sayBye() {
alert(`Bye ${this.name}`);
}
};
// usage:
class User {
constructor(name) {
this.name = name;
}
}
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;
}
}
Please note that the call to the parent method super.say() from sayHiMixin looks for
the method in the prototype of that mixin, not the class.
That’s because methods sayHi and sayBye were initially created in sayHiMixin . So their
[[HomeObject]] internal property references sayHiMixin , as shown on the picture
above.
EventMixin
The important feature of many browser objects (not only) can generate events. Events is a
great way to “broadcast information” to anyone who wants it. So let’s make a mixin that allows
to easily add event-related functions to any class/object.
● The mixin will provide a method .trigger(name, [...data]) to “generate an event”
when something important happens to it. The name argument is a name of the event,
optionally followed by additional arguments with event data.
● Also the method .on(name, handler) that adds handler function as the listener to
events with the given name. It will be called when an event with the given name triggers,
and get the arguments from .trigger call.
● …And the method .off(name, handler) that removes handler listener.
After adding the mixin, an object user will become able to generate an event "login" when
the visitor logs in. And another object, say, calendar may want to listen to such events to
load the calendar for the logged-in person.
Or, a menu can generate the event "select" when a menu item is selected, and other
objects may assign handlers to react on that event. And so on.
let eventMixin = {
/**
* Subscribe to event, usage:
* menu.on('select', function(item) { ... }
*/
on(eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},
/**
* Cancel the subscription, usage:
* menu.off('select', handler)
*/
off(eventName, handler) {
let handlers = this._eventHandlers && this._eventHandlers[eventName];
if (!handlers) return;
for (let i = 0; i < handlers.length; i++) {
if (handlers[i] === handler) {
handlers.splice(i--, 1);
}
}
},
/**
* Generate an event with the given name and data
* this.trigger('select', data1, data2);
*/
trigger(eventName, ...args) {
if (!this._eventHandlers || !this._eventHandlers[eventName]) {
return; // no handlers for that event name
}
● .on(eventName, handler) – assigns function handler to run when the event with
that name happens. Technically, there’s _eventHandlers property, that stores an array of
handlers for each event name. So it just adds it to the list.
● .off(eventName, handler) – removes the function from the handlers list.
● .trigger(eventName, ...args) – generates the event: all handlers from
_eventHandlers[eventName] are called, with a list of arguments ...args .
Usage:
// 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 on menu selection, we can listen to it with menu.on(...) .
And eventMixin mixin makes it easy to add such behavior to as many classes as we’d like,
without interfering with the inheritance chain.
Summary
Mixin – is a generic object-oriented programming term: a class that contains methods for other
classes.
Some other languages like e.g. Python allow to create mixins using multiple inheritance.
JavaScript does not support multiple inheritance, but mixins can be implemented by copying
methods into prototype.
We can use mixins as a way to augment a class by multiple behaviors, like event-handling as
we have seen above.
Mixins may become a point of conflict if they occasionally overwrite existing class methods. So
generally one should think well about the naming methods of a mixin, to minimize the probability
of that.
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 of other reasons.
Usually, a script “dies” (immediately stops) in case of an error, printing it to console.
But there’s a syntax construct try..catch that allows to “catch” errors and, instead of dying,
do something more reasonable.
The try..catch construct has two main blocks: try , and then catch :
try {
// code...
} catch (err) {
// error handling
}
It works like this:
1. First, the code in try {...} is executed.
2. If there were no errors, then catch(err) is ignored: the execution reaches the end of
try and then jumps over catch .
3. If an error occurs, then try execution is stopped, and the control flows to the beginning of
catch(err) . The err variable (can use any name for it) contains an error object with
details about what’s happened.
So, an error inside the try {…} block does not kill the script: we have a chance to handle it in
catch .
try {
} catch(err) {
} catch(err) {
try {
{{{{{{{{{{{{
} catch(e) {
alert("The engine can't understand this code, it's invalid");
}
The JavaScript engine first reads the code, and then runs it. The errors that occur on the
reading phrase are called “parse-time” errors and are unrecoverable (from inside that code).
That’s because the engine can’t understand the code.
So, try..catch can only handle errors that occur in the valid code. Such errors are
called “runtime errors” or, sometimes, “exceptions”.
⚠ 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 inside catch block has two main properties:
name
Error name. For an undefined variable that’s "ReferenceError" .
message
Textual message about error details.
There are other non-standard properties available in most environments. One of most widely
used and supported is:
stack
Current call stack: a string with information about the sequence of nested calls that led to the
error. Used for debugging purposes.
For instance:
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 ...
⚠ A recent addition
This is a recent addition to the language. Old browsers may need polyfills.
try {
// ...
} catch {
// error object omitted
}
Using “try…catch”
You can find more detailed information about JSON in the JSON methods, toJSON chapter.
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.
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.
Their syntax is:
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 0
}
And in our case, the absence of name could be treated as a syntax error also, assuming that
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 that “incorrect data” thing.
Like this:
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
}
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
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' );
}
The code has two ways of execution:
1. If you answer “Yes” to “Make an error?”, then try -> catch -> finally .
2. If you say “No”, then try -> finally .
The finally clause is often used when we start doing something 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, an 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.
Variables are local inside try..catch..finally
Please note that result and diff variables in the code above are declared before
try..catch .
Otherwise, if let were made inside the {...} block, it would only be visible inside of it.
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 right here, 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 jumps outside.
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 something else terrible.
Is there a way to react on such occurrences? We may want to log the error, show something to
the user (normally 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 special window.onerror property. It 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 .
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.
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 basic pattern of error handling: a catch block usually expects and knows how
to handle the particular error type, so it should rethrow errors it doesn’t know.
Even if we don’t have try..catch , most environments allow to setup a “global” error handler
to catch errors that “fall out”. In-browser that’s window.onerror .
✔ Tasks
Finally or just the code?
importance: 5
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 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.
Here’s an example of how a valid json may look:
But even if json is syntactically correct, that doesn’t mean that it’s a valid user, right? It may
miss the necessary data. For instance, it may not have name and age properties that are
essential for our users.
Our function readUser(json) will not only read JSON, but check (“validate”) the data. If
there are no required fields, or the format is wrong, then that’s an error. And that’s not a
SyntaxError , because the data is syntactically correct, but another kind of error. We’ll call it
ValidationError and create a class for it. An error of that kind should also carry the
information about the offending field.
Our ValidationError class should inherit from the built-in Error class.
That class is built-in, but we should have its approximate code before our eyes, to understand
what we’re extending.
So here you are:
// 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 = <nested calls>; // 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
}
1. In the line (1) we call the parent constructor. JavaScript requires us to call super in the
child constructor, so that’s obligatory. The parent constructor sets the message property.
2. The parent constructor also sets the name property to "Error" , so in the line (2) we
reset it to the right value.
// 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”, right?
There may occur different kinds of errors in the process. Right now we have SyntaxError
and ValidationError , but in the future readUser function may grow and probably
generate other kinds of errors.
The code which calls readUser should handle these errors. Right now it uses multiple if in
the catch block to check for different error types and rethrow the unknown ones. But if
readUser function generates several kinds of errors – then we should ask ourselves: do we
really want to check for all error types one-by-one in every code that calls readUser ?
Often the answer is “No”: the outer code wants to be “one level above all that”. It wants to have
some kind of “data reading error”. Why exactly it happened – is often irrelevant (the error
message describes it). Or, even better if there is a way to get error details, but only if we need
to.
So let’s make a new class ReadError to represent such errors. If an error occurs inside
readUser , we’ll catch it there and generate ReadError . We’ll also keep the reference to
the original error in 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 possible all
error types.
The approach is called “wrapping exceptions”, because we take “low level exceptions” and
“wrap” them into ReadError that is more abstract and more convenient to use for the calling
code. It is widely used in object-oriented programming.
Summary
● We can inherit from Error and other built-in error classes normally, just need to take care
of name property and don’t forget to call super .
● We can use instanceof to check for particular errors. It also works with inheritance. But
sometimes we have an error object coming from the 3rd-party library and there’s no easy
way to get the class. Then name property can be used for such checks.
● Wrapping exceptions is a widespread technique: a function handles low-level exceptions and
creates higher-level errors instead of various low-level ones. Low-level exceptions
sometimes become properties of that object like err.cause in the examples above, but
that’s not strictly required.
✔ Tasks
Create a class FormatError that inherits from the built-in SyntaxError class.
Usage example:
To solution
Promises, async/await
Introduction: callbacks
Many actions in JavaScript are asynchronous.
For instance, take a look at the function loadScript(src) :
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
The purpose of the function is to load a new script. When it adds the <script src="…"> to
the document, the browser loads and executes it.
We can use it like this:
The function is called “asynchronously,” because the action (script loading) finishes not now, but
later.
The call initiates the script loading, then the execution continues. While the script is loading, the
code below may finish executing, and if the loading takes time, other scripts may run meanwhile
too.
loadScript('/my/script.js');
// the code below loadScript doesn't wait for the script loading to finish
// ...
Now let’s say we want to use the new script when it loads. It probably declares new functions,
so we’d like to run them.
But if we do that immediately after the loadScript(…) call, that wouldn’t work:
Naturally, the browser probably didn’t have time to load the script. So the immediate call to the
new function fails. As of now, the loadScript function doesn’t provide a way to track the
load completion. The script loads and eventually runs, that’s all. But we’d like to know when it
happens, to use new functions and variables from that script.
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.
Here’s a runnable example with a real script:
Callback in callback
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.
Here’s an improved version of loadScript that tracks loading errors:
document.head.append(script);
}
The usage:
Once again, the recipe that we used for loadScript is actually quite common. It’s called the
“error-first callback” style.
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 (*)
}
});
}
})
}
});
As calls become more nested, the code becomes deeper and increasingly more difficult to
manage, especially if we have a 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.
So this way of coding isn’t very good.
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
a namespace cluttering here.
We’d like to have something better.
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
to which they can subscribe for updates. They can fill in their email addresses, so that when the
song becomes available, all subscribed parties instantly receive it. And even if something goes
very wrong, say, if plans to publish the song are cancelled, they will still be notified.
Everyone is happy, because the people don’t crowd you anymore, and fans, because they won’t
miss the single.
This is a real-life analogy for things we often have in programming:
1. A “producing code” that does something and takes time. For instance, the code loads 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 the promise is created,
this executor function runs automatically. It contains the producing code, that should eventually
produce a result. In terms of the analogy above: the executor is the “singer”.
When the executor finishes the job, it should call one of the functions that it gets as arguments:
● resolve(value) — to indicate that the job finished successfully:
● sets state to "fulfilled" ,
● sets result to value .
● reject(error) — to indicate that an error occurred:
● sets state to "rejected" ,
● sets result to error .
// after 1 second signal that the job is done with the result "done"
setTimeout(() => resolve("done"), 1000);
});
1. The executor is called automatically and immediately (by the new Promise ).
2. The executor receives two arguments: resolve and reject — these functions are pre-
defined by the JavaScript engine. So we don’t need to create them. We only should call one
of them when ready.
After one second of “processing” the executor calls resolve("done") to produce the result:
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.
The Promise that is either resolved or rejected is called “settled”, as opposed to a 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, we can call reject with any type of argument (just like
resolve ). But it is recommended to use Error objects (or objects that inherit from
Error ). The reasoning for that will soon become apparent.
For instance, this might happen when we start to do a job but then see that everything has
already been completed and cached.
That’s fine. We immediately have a resolved promise.
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 */ }
);
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 it 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 .
The good thing is: a .then handler is guaranteed to run whether the promise takes time or
settles it immediately.
Next, let’s see more practical examples of how promises can help us to write asynchronous
code.
Example: loadScript
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:
promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`)
);
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
Delay with a promise
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
}
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?
Promises provide a couple of recipes to do that.
}).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.
The whole thing works, because a call to promise.then returns a promise, so that we can
call the next .then on it.
When a handler returns a value, it becomes the result of that promise, so the next .then is
called with it.
To make these words more clear, here’s the start of the chain:
alert(result);
return result * 2; // <-- (1)
}) // <-- (2)
// .then…
The value returned by .then is a promise, that’s why we are able to add another .then at
(2) . When the value is returned in (1) , that promise becomes resolved, so the next handler
runs with the value.
A classic newbie error: technically we can also add many .then to a single promise.
This is not chaining.
For example:
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.
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
Normally, a value returned by a .then handler is immediately passed to the next handler. But
there’s an exception.
If the returned value is a promise, then the further execution is suspended until it settles. After
that, the result of that promise is given to the next .then handler.
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 returns new Promise(…) in the line (*) . After one second
it resolves, and the result (the argument of resolve , here it’s result*2 ) is passed on to
handler of the second .then in the line (**) . It shows 2 and does the same thing.
So the output is again 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 code is still “flat”, it
grows down, not to the right. There are no signs of “pyramid of doom”.
Please note that technically we can add .then directly to each loadScript , like this:
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, .then may return a so-called “thenable” object – an arbitrary object that
has method .then , and it will be treated the same way as a promise.
The idea is that 3rd-party libraries may implement “promise-compatible” objects of their
own. They can have extended set of methods, but also be compatible with native promises,
because they implement .then .
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 .then handler in the line (*) : if it has a callable
method named then , then it calls that method providing native functions resolve ,
reject as arguments (similar to 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 to integrate custom objects with promise chains without having to inherit
from Promise .
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 a method response.text() : it returns a promise
that resolves when the full text 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 we finish downloading it
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.
For instance, we can make one more request to GitHub, load the user profile and show the
avatar:
The code works, see comments about the details. Although, there’s a potential problem in it, a
typical error of those who begin to use promises.
Look at the line (*) : how can we do something after the avatar has finished showing and gets
removed? For instance, we’d like to show a form for editing that user or something else. As of
now, there’s no way.
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 makes it possible to plan actions after it. Even if we don’t plan to extend the chain now, we
may need it later.
Finally, we can split the code into reusable functions:
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.
Here’s a full picture:
✔ Tasks
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
Error handling with promises
Asynchronous actions may sometimes fail: in case of an error the corresponding promise
becomes rejected. For instance, fetch fails if the remote server is not available. We can use
.catch to handle errors (rejections).
Promise chaining is great at that aspect. When a promise rejects, the control jumps to the
closest rejection handler down the chain. That’s very convenient in practice.
For instance, in the code below the URL is wrong (no such 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)
Or, maybe, everything is all right with the site, but the response is not valid JSON:
fetch('/') // fetch works fine now, the server responds with the HTML page
.then(response => response.json()) // rejects: the page is HTML, not a valid json
.catch(err => alert(err)) // SyntaxError: Unexpected token < in JSON at position 0
The easiest way to catch all errors is to append .catch to the end of chain:
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, .catch doesn’t trigger at all, because there are no errors. But if any of the promises
above rejects (a network problem or invalid json or whatever), then it would catch it.
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.
For instance, this code:
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!
The "invisible try..catch " around the executor automatically catches the error and treats it
as a rejection.
This happens not only in the executor, 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 behaves like 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() {
/* never runs here */
}).catch(error => { // (**)
});
Then the execution jumps from the first .catch (*) to the next one (**) down the chain.
The promise returned by fetch rejects when it’s impossible to make a request. For instance,
a remote server is not available, or the URL is malformed. But if the remote server responds
with error 404, or even error 500, then it’s considered a valid response.
What if the server returns a non-JSON page with error 500 in the line (*) ? What if there’s no
such user, and GitHub returns a page with error 404 at (**) ?
fetch('no-such-user.json') // (*)
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`)) // (**)
.then(response => response.json())
.catch(alert); // SyntaxError: Unexpected token < in JSON at position 0
// ...
As of now, the code tries to load the response as JSON no matter what and dies with a syntax
error. You can see that by running the example above, as the file no-such-user.json
doesn’t exist.
That’s not good, because the error just falls through the chain, without details: what failed and
where.
So let’s add one more step: we should check the response.status property that has HTTP
status, and if it’s not 200, then throw an error.
loadJson('no-such-user.json') // (3)
.catch(alert); // HttpError: 404 for .../no-such-user.json
1. We make a custom class for HTTP Errors to distinguish them from other types of errors.
Besides, the new class has a constructor that accepts response object and saves it in the
error. So error-handling code will be able to access the response.
2. Then we put together the requesting and error-handling code into a function that fetches the
url and treats any non-200 status as an error. That’s convenient, because we often need
such logic.
3. Now alert shows a more helpful descriptive message.
The great thing about having our own class for errors is that we can easily check for it in error-
handling code using instanceof .
For instance, we can make a request, and then if we get 404 – ask the user to modify the
information.
The code below loads a user with the given name from GitHub. If there’s no such user, then it
asks for the correct name:
function demoGithubUser() {
let name = prompt("Enter a name?", "iliakan");
return loadJson(`https://api.github.com/users/${name}`)
.then(user => {
alert(`Full name: ${user.name}.`);
return user;
})
.catch(err => {
if (err instanceof HttpError && err.response.status == 404) {
alert("No such user, please reenter.");
return demoGithubUser();
} else {
throw err; // (*)
}
});
}
demoGithubUser();
Please note: .catch here catches all errors, but it “knows how to handle” only HttpError
404 . In that particular case it means that there’s no such user, and .catch just retries in that
case.
For other errors, it has no idea what could go wrong. Maybe a programming error or something.
So it just rethrows it in the line (*) .
Unhandled rejections
What happens when an error is not handled? For instance, after the rethrow (*) in the
example above.
Or we could just forget to append an error handler to the end of the chain, like here:
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 state becomes “rejected”, and the execution should jump to the
closest rejection handler. But there is no such handler in the examples above. So the error gets
“stuck”. There’s no code to handle it.
In practice, just like with a regular unhandled errors, it means that something has terribly gone
wrong.
What happens when a regular error occurs and is not caught by try..catch ? The script
dies. 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 similar ways to track unhandled
errors.
Summary
● .catch handles promise rejections of all kinds: be it a reject() call, or an error thrown
in a handler.
● We should place .catch exactly in places where we want to handle errors and know how
to handle them. The handler should analyze errors (custom error classes help) and rethrow
unknown ones.
● It’s ok not to use .catch at all, if there’s no way to recover from an error.
● In any case we should have the unhandledrejection event handler (for browsers, and
analogs for other environments), to track unhandled errors and inform the user (and probably
our server) about the them, so that our app never “just dies”.
And finally, if we have load-indication, then .finally is a great handler to stop it when the
fetch is complete:
function demoGithubUser() {
let name = prompt("Enter a name?", "iliakan");
return loadJson(`https://api.github.com/users/${name}`)
.finally(() => { // (2) stop the indication
document.body.style.opacity = '';
return new Promise(resolve => setTimeout(resolve)); // (*)
})
.then(user => {
alert(`Full name: ${user.name}.`);
return user;
})
.catch(err => {
if (err instanceof HttpError && err.response.status == 404) {
alert("No such user, please reenter.");
return demoGithubUser();
} else {
throw err;
}
});
}
demoGithubUser();
Here on the line (1) we indicate loading by dimming the document. The method doesn’t
matter, could use any type of indication instead.
When the promise is settled, be it a successful fetch or an error, finally triggers at the line
(2) and stops the indication.
There’s a little browser trick (*) with returning a zero-timeout promise from finally . That’s
because some browsers (like Chrome) need “a bit time” outside promise handlers to paint
document changes. So it ensures that the indication is visually stopped before going further on
the chain.
✔ 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.resolve
The syntax:
let promise = Promise.resolve(value);
Same as:
The method is used when we already have a value, but would like to have it “wrapped” into a
promise.
For instance, the loadCached function below fetches the url and remembers the result, so
that future calls on the same URL return it immediately:
function loadCached(url) {
let cache = loadCached.cache || (loadCached.cache = new Map());
if (cache.has(url)) {
return Promise.resolve(cache.get(url)); // (*)
}
return fetch(url)
.then(response => response.text())
.then(text => {
cache.set(url,text);
return text;
});
}
Promise.reject
The syntax:
Same as:
Let’s say we want to run many promises to execute in parallel, and wait till all of them are ready.
For instance, download several URLs in parallel and process the content when all are done.
That’s what Promise.all is for.
It takes an array of promises (technically can be any iterable, but usually an array) and returns a
new promise.
The new promise resolves when all listed promises are settled and has an array of their results.
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 relative order is the same. Even though the first promise takes the longest
time to resolve, it is still first in the array of results.
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 same):
let names = ['iliakan', 'remy', 'jeresig'];
Promise.all(requests)
.then(responses => {
// all responses are ready, we can show HTTP status codes
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // shows 200 for every url
}
return responses;
})
// map array of responses into 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)));
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 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 don’t watch them any more.
They will probably settle, but the result will be ignored.
Promise.all does nothing to cancel them, as there’s no concept of “cancellation” in
promises. In another chapter we’ll cover AbortController that can help with that, but
it’s not a part of the Promise API.
Promise.all(iterable) allows non-promise “regular” values in iterable
Normally, Promise.all(...) accepts an iterable (in most cases an array) of promises.
But if any of those objects is not a promise, it’s wrapped in Promise.resolve .
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
}),
2, // treated as Promise.resolve(2)
3 // treated as Promise.resolve(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 in 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 them all
Promise.allSettled waits for all promises to settle: even if one rejects, it waits for the
others. 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 interested in the others.
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://no-such-url'
];
Promise.allSettled(urls.map(url => fetch(url)))
.then(results => { // (*)
results.forEach((result, num) => {
if (result.status == "fulfilled") {
alert(`${urls[num]}: ${result.value.status}`);
}
if (result.status == "rejected") {
alert(`${urls[num]}: ${result.reason}`);
}
});
});
[
{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(v => ({
state: 'fulfilled',
value: v,
}), r => ({
state: 'rejected',
reason: r,
}))));
};
}
In this code, promises.map takes input values, turns into promises (just in case a non-
promise was passed) with p => Promise.resolve(p) , and then adds .then handler to
it.
That handler turns a successful result v into {state:'fulfilled', value:v} , and an
error r into {state:'rejected', reason:r} . That’s exactly the format of
Promise.allSettled .
Then we can use Promise.allSettled to get the results or all given promises, even if
some of them reject.
Promise.race
Similar to Promise.all , it takes an iterable of promises, but instead of waiting for all of them
to finish, it waits for the first result (or error), and goes on with it.
The syntax is:
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
So, the first result/error becomes the result of the whole Promise.race . After the first settled
promise “wins the race”, all further results/errors are ignored.
Summary
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.
To be more precise, we create a wrapper-function that does the same, internally calling the
original one, but returns a promise.
Such transforms are often needed in real-life, as many functions and libraries are callback-
based. But promises are more convenient. So it makes sense to promisify those.
For instance, we have loadScript(src, callback) from the chapter Introduction:
callbacks.
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
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 .
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 => ...)
In some cases, err may be absent at all: callback(result) , or there’s something exotic
in the callback format, then we can promisify such functions 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).
Or, to say that simply, when a promise is ready, its .then/catch/finally handlers are put
into the queue. They are not executed yet. JavaScript engine takes a task from the queue and
executes it, when it becomes free from the current code.
That’s why “code finished” in the example above shows first.
Promise handlers always go through that internal queue.
If there’s a chain with multiple .then/catch/finally , then every one of them is executed
asynchronously. 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 work after
promise done ?
Promise.resolve()
.then(() => alert("promise done!"))
.then(() => alert("code finished"));
Unhandled rejection
Remember “unhandled rejection” 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:
// Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));
Now, if you run it, we’ll see Promise Failed! message first, and then caught .
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:
The word “async” before a function means one simple thing: a function always returns a
promise. Even If a function actually returns a non-promise value, prepending the function
definition with the “async” keyword directs JavaScript to automatically wrap that value in a
resolved promise.
For instance, the code above returns a resolved promise with the result of 1 , let’s test it:
f().then(alert); // 1
…We could explicitly return a promise, that would be the same as:
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 till 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.
await won’t work in the top-level code
People who are just starting to use await tend to forget the fact that we can’t use await
in top-level code. For example, this will not work:
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
Like promise.then , await allows to use thenable objects (those with a callable then
method). The idea is that a 3rd-party object may not be a promise, but promise-compatible:
if it supports .then , that’s enough to use with await .
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 native
functions resolve , reject as arguments. 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 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. So await will wait, and
then throw 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.
When we use async/await , we rarely need .then , because await handles the
waiting for us. And we can use a regular try..catch instead of .catch . That’s usually
(not always) more convenient.
But at the top level of the code, when we’re outside of any async function, we’re
syntactically unable to use await , so it’s a normal practice to add .then/catch to
handle the final result or falling-through errors.
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, so we can assign it to a value.
Together they provide a great framework to write asynchronous code that is easy both to read
and write.
✔ Tasks
Rewrite the one of examples 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, possibly an infinite number of 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;
}
When generateSequence() is called, it does not execute the code. Instead, it returns a
special object, called “generator”.
The main method of a generator is next() . When called, it resumes execution till the nearest
yield <value> statement. Then the execution pauses, and the 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;
}
Let’s call generator.next() again. It resumes the execution and returns the next yield :
And, if we call it the third time, then the execution reaches 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 generator.next() don’t make sense any more. If we make them, they return the
same object: {done: true} .
There’s no way to “roll back” a generator. But we can create another one by calling
generateSequence() .
So far, the most important thing to understand is that generator functions, unlike regular
function, do not run the code. They serve as “generator factories”. Running function*
returns a generator, and then we ask it for values.
But usually the first syntax is preferred, as the star * denotes that it’s a generator function,
it describes the kind, not the name, so it should stick with the function keyword.
As you probably already guessed looking at the next() method, generators are iterable.
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
That’s a much better-looking way to work with generators than calling .next().value , right?
…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;
}
Naturally, 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 into 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 .
alert([...range]); // 1,2,3,4,5
alert(sequence); // 1, 2, 3, 4, 5
We can get the best from both worlds by providing a generator as Symbol.iterator :
let range = {
from: 1,
to: 5,
That’s not a coincidence, of course. Generators aim to make iterables easier, so we can see
that.
The last variant with a generator is much more concise than the original iterable code, and
keeps the same functionality.
That surely would require a break in for..of , otherwise the loop would repeat forever
and hang.
Generator composition
Then we plan to create passwords by selecting characters from it (could add syntax characters
as well), but need to generate the sequence 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, we can do better, like this:
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 special yield* directive in the example is responsible for the composition. It delegates
the execution to another generator. Or, to say it simple, it runs generators and transparently
forwards their yields outside, as if they were done by the calling generator itself.
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 works even if the flow of values from the nested generator is infinite. It’s simple and doesn’t
use extra memory to store intermediate results.
Till this moment, generators were like “iterators on steroids”. And that’s how they are often
used.
function* gen() {
// Pass a question to the outer code and wait for an answer
let result = yield "2 + 2?"; // (*)
alert(result);
}
1. The first call generator.next() is always without an argument. It starts the execution
and returns the result of the first yield (“2+2?”). At this point the generator pauses the
execution (still on that 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 to
calculate the value. This is also a valid code:
// resume the generator after some time
setTimeout(() => generator.next(4), 1000);
The syntax may seem a bit odd. It’s quite uncommon for a function and the calling code to pass
values around to each other. But that’s exactly what’s going on.
To make things more obvious, here’s another example, with more calls:
function* gen() {
let ask1 = yield "2 + 2?";
alert(ask1); // 4
alert(ask2); // 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 .
function* gen() {
try {
let result = yield "2 + 2?"; // (1)
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 the line (2) leads to an exception in the line (1) with
yield . In the example above, try..catch catches it and shows.
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.
Also, in the next chapter we’ll learn async generators, which are used to read streams of
asynchronously generated data in for loop.
In web-programming we often work with streamed data, e.g. need to fetch paginated results, so
that’s a 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 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
Async iterators
Asynchronous iterators are similar to regular iterators, with a few syntactic differences.
“Regular” iterable object, as described in the chapter Iterables, look 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 :
Normally, we can’t use await in generators. 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.
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’s recall an example from the chapter Generators:
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 APIs that deliver paginated data. For instance, when we need a list of
users, then we can fetch it page-by-page: a request returns a pre-defined count (e.g. 100
users), and provides an URL to the next page.
The 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.
What we’d like to have is a simpler API: an iterable object with commits, so that we could go
over them like this:
We’d like fetchCommits to get 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
});
const body = await response.json(); // (2) parses response as JSON (array of commits)
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 can 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 this:
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;
console.log(commit.author.login);
})();
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 .
We can use async generators to process such data, but it’s worth to mention that there’s also
another API called Streams, that provides special interfaces to transform the data and to pass it
from one stream to another (e.g. download from one place and immediately send elsewhere).
Streams API not a part of JavaScript language standard. Streams and async generators
complement each other, both are great ways to handle async data flows.
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 useful 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.
What is a module?
// sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
…Then another file may import and use it:
// main.js
import {sayHi} from './sayHi.js';
alert(sayHi); // function...
sayHi('John'); // Hello, John!
In this tutorial we concentrate on the language itself, but we use browser as the demo
environment, so let’s see how to use modules in the browser.
As modules support special keywords and features, we must tell the browser that a script
should be treated as module, by using the attribute <script type="module"> .
Like this:
https://plnkr.co/edit/rZprEnNmZyqNtNqxtijS?p=preview
The browser automatically fetches and evaluates imported modules, and then runs the script.
<script type="module">
a = 5; // error
</script>
Here we can see it in the browser, but the same is true for any module.
Module-level scope
Each module has its own top-level scope. In other words, top-level variables and functions from
a module are not seen in other scripts.
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 directly into hello.js instead of index.html .
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.
First, if executing a module code brings side-effects, like showing a message, then importing it
multiple times will trigger it only once – the first time:
// alert.js
alert("Module is evaluated!");
// 1.js
import `./alert.js`; // Module is evaluated!
// 2.js
import `./alert.js`; // (nothing)
In practice, top-level module code is mostly used for initialization. We create data structures,
pre-fill them, and if we want something to be reusable – export it.
// 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 is great for modules that require configuration. We can set required properties on
the first import, and then in further imports it’s ready.
For instance, admin.js module may provide certain functionality, but expect the credentials
to come into the admin object from outside:
// admin.js
export let admin = { };
Now, in init.js , the first script of our app, we set admin.name . Then everyone will see it,
including calls made from inside admin.js itself:
// 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 those for now if you’re reading for the first time, or if you don’t use
JavaScript in a browser.
In other words:
● external module scripts <script type="module" src="..."> don’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 works before the first! So we’ll see undefined first,
and then object .
That’s because modules are deferred, so way wait for the document to be processed. The
regular scripts runs immediately, so we saw its output first.
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 transparent overlays
or “loading indicators”, or otherwise ensure that the visitor won’t be confused by that.
For example, the script below has async , so it doesn’t wait for anyone.
It performs the import (fetches ./analytics.js ) and runs when ready, even if HTML
document is not finished yet, or if other scripts are still pending.
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
There are two notable differences of external module scripts:
<!-- 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: * (may use site domain instead of * ) to indicate
that the fetch is allowed.
<!-- another-site.com must supply Access-Control-Allow-Origin -->
<!-- otherwise, the script won't execute -->
<script type="module" src="http://another-site.com/their.js"></script>
Certain environments, like Node.js or bundle tools allow bare modules, without any path, as
they have own ways for finding modules and hooks to fine-tune them. But browsers do not
support bare modules yet.
Compatibility, “nomodule”
Old browsers do not understand type="module" . Scripts of the unknown type are just
ignored. For them, it’s possible to provide a fallback using nomodule attribute:
<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>
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:
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:
That said, native modules are also usable. So we won’t be using Webpack here: you can
configure it later.
Summary
So, generally, when we use modules, each module implements the functionality and exports it.
Then we use import to directly import it where it’s needed. 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.
For instance, here all exports are valid:
// 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;
}
}
// say.js
function sayHi(user) {
alert(`Hello, ${user}!`);
}
function sayBye(user) {
alert(`Bye, ${user}!`);
}
Import *
Usually, we put a list of what to import into import {...} , like this:
// main.js
import {sayHi, sayBye} from './say.js';
But if the list is long, we can import everything as an object using import * as <obj> , for
instance:
// 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?
Let’s say, we added a 3rd-party library lib.js to our project with many functions:
// lib.js
export function sayHi() { ... }
export function sayBye() { ... }
export function becomeSilent() { ... }
// main.js
import {sayHi} from './lib.js';
…Then the optimizer will automatically detect it and totally 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 lib.sayHi() .
3. Explicit imports give 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 same for sayBye :
// main.js
import {sayHi as hi, sayBye as bye} from './say.js';
Export “as”
// say.js
...
export {sayHi as hi, sayBye as bye};
// main.js
import * as say from './say.js';
export default
So far, we’ve seen how to import/export multiple things, optionally “as” other names.
In practice, modules contain either:
● A library, pack of functions, like lib.js .
● Or an entity, like class User is described in user.js , the whole module has only this
class.
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 syntax to make “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 imports and
doesn’t need them for the default one.
Another thing to note is that named exports must (naturally) have a name, while export
default may be anonymous.
Not giving a name is fine, because export default is only one per file. Contrary to that,
omitting a name for named imports would be an error:
“Default” alias
The “default” keyword is used as an “alias” for the default export, for standalone exports and
other scenarios when we need to reference it.
For example, if we already have a function declared, that’s how to export default it
(separately from the definition):
function sayHi(user) {
alert(`Hello, ${user}!`);
}
export {sayHi as default}; // same as if we added "export default" before the function
Or, let’s say a module user.js exports one main “default” thing and a few named ones
(rarely the case, but happens):
// 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');
Or, if we consider importing * as an object, then the default property is exactly the default
export:
// main.js
import * as user from './user.js';
Also, named exports enforce us to use exactly the right name to import:
So, there’s a little bit more freedom that can be abused, so that team members may use
different names for the same thing.
Usually, to avoid that and keep the code consistent, there’s a rule that imported variables
should correspond to file names, e.g:
Another solution would be to use named exports everywhere. 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:
export {sayHi} from './say.js';
export {default as User} from './user.js';
What’s the point, why that’s needed? Let’s see a practical use case.
Imagine, we’re writing a “package”: a folder with a lot of modules, mostly needed internally, with
some of the functionality exported outside (tools like NPM allow to publish and distribute
packages, but here it doesn’t matter).
A directory structure could be like this:
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. They should not 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.
Now, as the actual exported functionality is scattered among the package, we can gather and
“re-export” it in auth/index.js :
// auth/index.js
import {login, logout} from './helpers.js';
export {login, logout};
// auth/index.js
export {login, logout} from './helpers.js';
// or, to re-export all helpers, we could use:
// export * from './helpers.js';
The default should be mentioned explicitly only when re-exporting: import * as obj
works fine. It imports the default export as obj.default . So there’s a slight asymmetry
between import and export constructs here.
Summary
Import:
●
Named exports from module:
● import {x [as y], ...} from "mod"
● Default export:
● import x from "mod"
● import {default as x} from "mod"
● Everything:
● import * as obj from "mod"
●
Import the module (it runs), but do not assign it to a variable:
● import "mod"
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?
We’ll see dynamic imports in the next chapter.
Dynamic imports
Export and import statements that we covered in previous chapters are called “static”.
That’s because they are indeed static. The syntax is very 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 together,
unused exports can be removed (“tree-shaken”). That’s possible only because the structure of
imports/exports is simple and fixed.
But how can we import a module dynamically, on-demand?
The import(module) function can be called from anywhere. It returns a promise that
resolves into a module object.
The usage pattern looks like this:
import(modulePath)
.then(obj => <module object>)
.catch(err => <loading error, no such module?>)
// say.js
export function hi() {
alert(`Hello`);
}
hi();
bye();
// say.js
export default function() {
alert("Module loaded (export default)!");
}
To import it, we need to get default property of the module object, as explained in the
previous chapter.
say();
https://plnkr.co/edit/zmyUQYZbECmhHGIJzKSd?p=preview
So, dynamic imports are very simple to use, and they allow to import modules at run-time.
Also, 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() ).
That means that import doesn’t inherit from Function.prototype so we cannot call or
apply it.
Miscellaneous
Proxy and Reflect
A proxy 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:
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 .
The proxy is a special “exotic object”. It doesn’t have “own” properties. With an empty handler it
transparently forwards operations to target .
There’s a list of internal object operations in the Proxy specification . A proxy can intercept
any of these, we just need to add a handler method.
In the table below:
● Internal Method is the specification-specific name for the operation. For example,
[[Get]] is the name of the internal, specification-only method of reading a property. The
specification describes how this is done at the very lowest level.
● Handler Method is a method name that we should add to proxy handler to trap the
operation and perform custom actions.
Object.keys , Object.getOwnPropertyNames ,
[[OwnPropertyKeys]] ownKeys
Object.getOwnPropertySymbols , iteration keys
⚠ Invariants
JavaScript enforces some invariants – conditions that must be fulfilled by internal methods
and traps.
In other words, reading prototype of a proxy must always return the prototype of the
target object. The getPrototypeOf trap may intercept this operation, but it must follow
this rule, not do something crazy.
Invariants ensure correct and consistent behavior of language features. The full invariants
list is in the specification , you probably won’t violate them, if not doing something weird.
To intercept the reading, the handler should have a method get(target, property,
receiver) .
Let’s wrap it into a proxy that traps reading and returns the default value if there’s no such
property:
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (no such value)
The approach is generic. We can use Proxy to implement any logic for “default” values.
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 non-translated is usually better than undefined . So let’s make a
non-translated phrase the default value 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.
The set trap triggers when a property is written: set(target, property, value,
receiver)
●
target – is the target object, the one passed as the first argument to new Proxy ,
● property – property name,
●
value – property value,
● receiver – same as in get trap, only matters if the property is a setter.
The set trap should return true if setting is successful, and false otherwise (leads to
TypeError ).
numbers.push(1);
numbers.push(2);
alert("Length is: " + numbers.length); // 2
Please note: the built-in functionality of arrays is still working! The length property auto-
increases when values are added. Our proxy doesn’t break anything.
Also, we don’t have to override value-adding array methods like push and unshift , and so
on! Internally, they use [[Set]] operation, that’s intercepted by the proxy.
If it returns a falsy value (or doesn’t return anything), that triggers TypeError .
There’s a widespread convention that properties and methods prefixed by an underscore _ are
internal. They shouldn’t be accessible from outside the object.
let user = {
name: "John",
_password: "secret"
};
alert(user._password); // secret
let user = {
name: "John",
_password: "***"
};
Please note the important detail in get trap, in the line (*) :
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
If an object method is called, such as user.checkPassword() , it must be able to access
_password :
user = {
// ...
checkPassword(value) {
// object method must be able to read _password
return value === this._password;
}
}
Normally, user.checkPassword() call gets proxied user as this (the object before dot
becomes this ), so when it tries to access this._password , the property protection kicks
in and throws an error. So we bind it to target in the line (*) . Then all operations from that
function directly reference the object, without any property protection.
That solution is not ideal, as the 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.
As an object may be proxied multiple times (multiple proxies may add different “tweaks” to the
object), weird bugs may follow.
So, for complex objects with methods such proxy shouldn’t be used.
Such properties have their own issues though. In particular, they are not inherited.
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) would return a function that
forwards all calls to f after ms milliseconds.
function sayHi(user) {
alert(`Hello, ${user}!`);
}
As you can see, 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. So if
we have a property on the original function, we can’t access it after wrapping:
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
(*) .
There exist other traps, but probably you’ve already got the idea.
Reflect
let user = {
name: "John",
};
In most cases, we can do the same thing without Reflect . But we may miss some peculiar
aspects.
Consider the following example, it doesn’t use Reflect and doesn’t work right.
We have a proxied user object and inherit from it, then use a getter:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let admin = {
__proto__: user,
_name: "Admin"
};
// Expected: Admin
alert(admin.name); // Guest (?!?)
As you can see, the result is incorrect! The admin.name is expected to be "Admin" , not
"Guest" ! Without the proxy, it would be "Admin" , looks like the proxying “broke” our object.
Why this happens? That’s easy to understand if we explore what’s going on during the call in
the last line of the code.
1. There’s no name property in admin , so admin.name call goes to admin prototype.
2. The prototype is the proxy, so its get trap intercepts the attempt to read name .
3. In the line (*) it returns target[prop] , but what is the target ?
● The target , the first argument of get , is always the object passed to new Proxy ,
the original user .
●
So, target[prop] invokes the getter name with this=target=user .
● Hence the result is "Guest" .
How to fix it? That’s what the receiver , the third argument of get is for! It holds the correct
this . We just need to call Reflect.get to pass it on.
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let admin = {
__proto__: user,
_name: "Admin"
};
alert(admin.name); // Admin
Now the receiver holding the correct this is passed to getter by Reflect.get in the
line (*) , so it works correctly.
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 are a great way to alter or tweak the behavior of the existing objects, including built-in
ones, such as arrays.
Still, it’s not perfect. There are limitations.
These are like properties, but reserved for internal purposes. Built-in methods access them
directly, not via [[Get]]/[[Set]] internal methods. So Proxy can’t intercept that.
For example:
An attempt to set a value into a proxied Map fails, for the reason related to its internal
implementation .
Internally, a Map stores all data in its [[MapData]] internal slot. The proxy doesn’t have
such slot. The set method tries to access this.[[MapData]] internal property, 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 user.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, explained previously: it exposes the original object to the
method, potentially allowing it to be passed further and breaking other proxied functionality.
Proxy != target
Proxy and the original object are different objects. That’s natural, right?
So if we store the original object somewhere, and then proxy it, then things might break:
class User {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}
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’t intercept a strict equality test ===
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 proxy will forward
operations to object, and we also get a special method to disable it.
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 the proxy:
let object = {
data: "Valuable data"
};
revokes.set(proxy, revoke);
The benefit of such approach is that we don’t have to carry revoke around. We can get it from
the map by proxy when needeed.
Using WeakMap instead of Map here, because it should not 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 (we don’t need its revoke in that case).
References
● Specification: Proxy .
● MDN: Proxy .
Summary
Proxy is a wrapper around an object, that forwards operations to the object, optionally
trapping some of them.
…Then we should use proxy everywhere instead of target . A proxy doesn’t have its own
properties or methods. It traps an operation if the trap is provided or forwards it to target
object.
We can trap:
● Reading ( get ), writing ( set ), deleting ( deleteProperty ) a property (even a non-
existing one).
● Calling functions with new ( construct trap) and without new ( apply 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.
Write a function wrap(target) that takes an object target and return a proxy instead with
that functionality.
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 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
function makeObservable(target) {
/* your code */
}
Whenever a property changes, handler(key, value) is called with the name and value o
the property.
P.S. In this task, please handle only writing to a property. Other operations can be implemented
in a similar way. P.P.S. You might want to introduce a global variable or a global structure to
store handlers. That’s fine here. In real life, such function lives in a module, that has its own
global scope.
To solution
For example:
For example:
The 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.
Still, if you’re sure you need to dynamically eval a string of code, please note that its ability to
access outer variables has side-effects.
Code minifiers (tools used before JS gets to production, to compress it) replace local variables
with shorter ones for brewity. That’s usually safe, but not if eval is used, as it may reference
them. So minifiers don’t replace all local variables that might be visible from eval . That
negatively affects code compression ratio.
Using outer local variables inside eval is a bad programming practice, as it makes
maintaining the code more difficult.
let x = 1;
{
let x = 5;
window.eval('alert(x)'); // 1 (global variable)
}
If your code needs local variables, execute it with 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.
To solution
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 + 0 = "10" // (1)
"" - 1 + 0 = -1 // (2)
true + false = 1
6 / "3" = 2
"2" * "3" = 6
4 + 5 + "px" = "9px"
"$" + 4 + 5 = "$45"
"4" - 2 = 2
"4px" - 2 = NaN
7 / 0 = Infinity
" -9 " + 5 = " -9 5" // (3)
" -9 " - 5 = -14 // (4)
null + 1 = 1 // (5)
undefined + 1 = NaN // (6)
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.
To formulation
Operators
● a = 2
● b = 2
● c = 2
● d = 1
let a = 1, b = 1;
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. See (4).
7. Strict equality of different types.
To formulation
A simple page
JavaScript-code:
let name = prompt("What is your name?", "");
alert(name);
<!DOCTYPE html>
<html>
<body>
<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';
</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
What's the result of OR?
To formulation
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
The answer: 3 .
null || 3 || 4
To formulation
To formulation
To formulation
// 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 opimize it. For instance, we could look for the divisors from 2
to square root of i . But anyway, if we want to be really efficient for large intervals, we
need to change the approach and rely on advanced maths and complex algorithms like
Quadratic sieve , 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
Rewrite "if" into "switch"
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?');
}
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 an integer greater than 0`);
} else {
alert( pow(x, n) );
}
To formulation
ask(
"Do you agree?",
() => alert("You agreed."),
() => alert("You canceled the execution.")
);
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:
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?
In other words, user stores a reference to the object. And it can’t be changed. But the
content of the object can.
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
Multiply numeric properties by 2
function multiplyNumeric(obj) {
for (let key in obj) {
if (typeof obj[key] == 'number') {
obj[key] *= 2;
}
}
}
To formulation
Syntax check
Error!
Try it:
let user = {
name: "John",
go: function() { alert(this.name) }
}
(user.go)() // error!
The error message in most browsers does not give understanding what went wrong.
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.
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.
The value of this is one for the whole function, code blocks and object literals do not
affect it.
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
let ladder = {
step: 0,
up() {
this.step++;
return this;
},
down() {
this.step--;
return this;
},
showStep() {
alert( this.step );
return this;
}
}
ladder.up().up().down().up().down().showStep(); // 1
We also can write a single call per line. For long chains it’s more readable:
ladder
.up()
.up()
.down()
.up()
.down()
.showStep(); // 1
To formulation
Constructor, operator "new"
So they can, for instance, return the same externally defined object obj :
To formulation
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
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:
So, without strict mode, in the last line str has no trace of the property.
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
A random number from min to max
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
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:
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 :
alert( randomInteger(1, 3) );
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:
function ucFirst(str) {
if (!str) return str;
To formulation
To make the search case-insensitive, let’s bring the string to lower case and then
search:
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.
function truncate(str, maxlength) {
return (str.length > maxlength) ?
str.slice(0, maxlength - 1) + '…' : str;
}
To formulation
function extractCurrencyValue(str) {
return +str.slice(1);
}
To formulation
Arrays
Is array copied?
The result is 4 :
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.
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;
}
alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5
alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11
alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3
alert( getMaxSubSum([1, 2, 3]) ); // 6
alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100
The solution has a time complexety of O(n2) . 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;
}
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
Translate border-left-width to borderLeftWidth
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) {
}
let arr = [5, 3, 8, 1];
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();
}
alert( sorted );
alert( arr );
To formulation
● Please note how methods are stored. They are simply added to the internal object.
● All tests and numeric conversions are done in the calculate method. In future it
may be extended to support more complex expressions.
function Calculator() {
let 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
function sortByAge(arr) {
arr.sort((a, b) => a.age > b.age ? 1 : -1);
}
sortByAge(arr);
Shuffle an array
function shuffle(array) {
array.sort(() => Math.random() - 0.5);
}
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
[array[i], array[j]] = [array[j], array[i]]; // swap elements
}
}
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[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;
}
alert( getAverageAge(arr) ); // 28
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;
}
let strings = ["Hare", "Krishna", "Hare", "Krishna",
"Krishna", "Krishna", "Hare", "Hare", ":-O"
];
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, Set, WeakMap and WeakSet we’ll see how to optimize it.
To formulation
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.
Here we could also use a plain object instead of the Map , because keys are strings.
return Object.values(obj);
}
alert( aclean(arr) );
To formulation
Iterable keys
map.set("name", "John");
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. We can’t get
“all read messages” directly. But we can do it by iterating over all messages and filtering
those that are in the set.
P.S. Adding a property of our own to each message may be dangerous if messages are
managed by someone else’s code, but we can make it a symbol to evade conflicts.
Like this:
Now even if someone else’s code uses for..in loop for message properties, our
secret flag won’t appear.
To formulation
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
To formulation
Object.keys, values, entries
function sumSalaries(salaries) {
let sum = 0;
for (let salary of Object.values(salaries)) {
sum += salary;
}
let salaries = {
"John": 100,
"Pete": 300,
"Mary": 250
};
Or, optionally, we could also get the sum using Object.values and reduce :
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;
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.
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
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:
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
To get the number of milliseconds till tomorrow, we can from “tomorrow 00:00:00”
substract the current date.
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;
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;
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}`
}
}
To formulation
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) );
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
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.
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 .
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)
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.
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 loose such ability.
Talking about good variable names, list here is the list itself. The first element of it.
And it should remain like that. That’s clear and reliable.
From the other side, the role of tmp is exclusively a list traversal, like i in the for
loop.
Recursive solution
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);
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
So they have independent outer Lexical Environments, each one has its own count .
To formulation
Counter object
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 through function
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.
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.
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;
}
Open the solution with tests in a sandbox.
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
let i = 0;
function count() {
if (i == 1000000000) {
alert("Done in " + (Date.now() - start) + 'ms');
clearInterval(timer);
}
To formulation
Any setTimeout will run only after the current code has finished.
The i will be the last one: 100000000 .
let i = 0;
To formulation
Spy decorator
Here we can use calls.push(args) to store all arguments in the log and
f.apply(this, args) to forward the call.
function spy(func) {
function wrapper(...args) {
wrapper.calls.push(args);
return func.apply(this, arguments);
}
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.
We still can pass the right this by using an intermediate variable, but that’s a little bit
more cumbersome:
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;
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 memorized in savedArgs/savedThis . Please note that
both the context and the arguments are equally important and should be memorized.
We need them simultaneously to reproduce the call.
3. …Then after ms milliseconds pass, setTimeout triggers. The cooldown state is
removed ( isThrottled = false ). And if we had ignored calls, then wrapper is
executed with last memorized arguments and context.
The 3rd step runs not func , but wrapper , because we not only need to execute
func , but once again enter the cooldown state and setup the timeout to reset it.
To formulation
Function binding
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 result of bind is another object. It does not have the test property.
To formulation
The error occurs because ask gets functions loginOk/loginFail without the
object.
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 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
Where it writes?
Property lookup and execution are two different things. The method rabbit.eat is
first found in the prototype, then executed with this=rabbit
To formulation
2.
3.
4.
Then it calls push on it, adding the food into the stomach of the prototype.
Every time the stomach is taken from the prototype, then stomach.push modifies it
“at place”.
Please note that such thing doesn’t happen in case of a simple assignment
this.stomach= :
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 evade 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, are usually written into that object. That prevents such problems.
To formulation
F.prototype
Changing "prototype"
Answers:
1.
true .
2.
false .
Objects are assigned by reference. The object from Rabbit.prototype is not
duplicated, it’s still a single object is referenced both by Rabbit.prototype and
by the [[Prototype]] of rabbit .
So when we change its content through one reference, it is visible through the other
one.
3.
true .
All delete operations are applied directly to the object. Here delete
rabbit.eats tries to remove eats property from rabbit , but it doesn’t have it.
So the operation won’t have any effect.
4.
undefined .
The property eats is deleted from the prototype, it doesn’t exist any more.
To formulation
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 = {}; // (*)
let user = new User('John');
let user2 = new user.constructor('Pete');
At the end, we have let user2 = new Object('Pete') . The built-in Object
constructor ignores arguments, it always creates an empty object – that’s what we have
in user2 after all.
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 );
}
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
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);
}
}
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);
}
};
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 extends Object {}
class Rabbit {}
To formulation
Class checking: "instanceof"
Strange instanceof
But instanceof does not care about the function, but rather about its prototype ,
that it matches against the prototype chain.
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.
To formulation
To formulation
Introduction: callbacks
To formulation
Promise
Re-resolve a promise?
The second call to resolve is ignored, because only the first call of
reject/resolve is taken into account. Further calls are ignored.
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
The short answer is: no, they are not the equal:
promise
.then(f1)
.catch(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;
}
throw new Error(response.status);
}
loadJson('no-such-user.json')
.catch(alert); // Error: 404 (4)
Notes:
1.
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’s fine for this context. But then we loose ability to iterate with for..of and to use
generator composition, that may be useful elsewhere.
To formulation
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); // Error: 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
Part 2
Browser: Document,
Events, Interfaces
Ilya Kantor
Built at July 10, 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.
● Document
● Browser environment, specs
●
DOM tree
●
Walking the DOM
● Searching: getElement*, querySelector*
● Node properties: type, tag and contents
● Attributes and properties
● Modifying the document
● Styles and classes
● Element size and scrolling
● Window sizes and scrolling
● Coordinates
● Introduction to Events
● Introduction to browser events
● Bubbling and capturing
● Event delegation
● Browser default actions
● Dispatching custom events
● UI Events
● Mouse events basics
● Moving: mouseover/out, mouseenter/leave
● Drag'n'Drop with mouse events
●
Keyboard: keydown and keyup
● Scrolling
● Forms, controls
● Form properties and methods
● Focusing: focus/blur
●
Events: change, input, cut, copy, paste
● Forms: event and method submit
● Document and resource loading
● Page: DOMContentLoaded, load, beforeunload, unload
● Scripts: async, defer
● Resource loading: onload and onerror
● Miscellaneous
● Mutation observer
●
Selection and Range
● Event loop: microtasks and macrotasks
Learning how to manage the browser page: add elements, manipulate their size
and position, dynamically create interfaces and interact with the visitor.
Document
Here we’ll learn to manipulate a web-page using JavaScript.
function sayHi() {
alert("Hello");
}
There are more window-specific methods and properties, we’ll cover them later.
The document object gives access to the page content. We can change or
create anything on the page using it.
For instance:
CSSOM is used together with DOM when we modify style rules for the
document. In practice though, CSSOM is rarely required, because usually
CSS rules are static. We rarely need to add/remove CSS rules from
JavaScript, so we won’t cover it right now.
Browser Object Model (BOM) are additional objects provided by the browser (host
environment) to work with everything except the document.
For instance:
●
The navigator object provides background information about the browser
and the operating system. There are many properties, but the two most widely
known are: navigator.userAgent – about the current browser, and
navigator.platform – about the platform (can help to differ between
Windows/Linux/Mac etc).
●
The location object allows us to read the current URL and can redirect the
browser to a new one.
Summary
DOM specification
Describes the document structure, manipulations and events, see
https://dom.spec.whatwg.org .
CSSOM specification
Describes stylesheets and style rules, manipulations with them and their binding
to documents, see https://www.w3.org/TR/cssom-1/ .
HTML specification
Describes the HTML language (e.g. tags) and also the BOM (browser object
model) – various browser functions: setTimeout , alert , location and so
on, see https://html.spec.whatwg.org . It takes the DOM specification and
extends it with many additional properties and methods.
Please note these links, as there’s so much stuff to learn it’s impossible to cover
and remember everything.
When you’d like to read about a property or a method, the Mozilla manual at
https://developer.mozilla.org/en-US/search is also a nice resource, but the
corresponding spec may be better: it’s more complex and longer to read, but will
make your fundamental knowledge sound and complete.
Now we’ll get down to learning DOM, because the document plays the central role
in the UI.
DOM tree
The backbone of an HTML document are tags.
According to Document Object Model (DOM), every HTML-tag is an object.
Nested tags are called “children” of the enclosing one.
The text inside a tag it is an object as well.
An example of DOM
<!DOCTYPE HTML>
<html>
<head>
<title>About elks</title>
</head>
<body>
The truth about elks.
</body>
</html>
The DOM represents HTML as a tree structure of tags. Here’s how it looks:
▾ HTML
▾ HEAD
#text ↵␣␣␣␣
▾ TITLE
#text About elks
#text ↵␣␣
#text ↵␣␣
▾ BODY
#text The truth about elks.
Tags are called element nodes (or just elements). Nested tags become children of
the enclosing ones. As a result we have a tree of elements: <html> is at the
root, then <head> and <body> are its children, etc.
The text inside elements forms text nodes, labelled as #text . A text node
contains only a string. It may not have children and is always a leaf of the tree.
For instance, the <title> tag has the text "About elks" .
Please note the special characters in text nodes:
● a newline: ↵ (in JavaScript known as \n )
●
a space: ␣
Spaces and newlines – are totally valid characters, they form text nodes and
become a part of the DOM. So, for instance, in the example above the <head>
tag contains some spaces before <title> , and that text becomes a #text
node (it contains a newline and some spaces only).
There are only two top-level exclusions:
1. Spaces and newlines before <head> are ignored for historical reasons,
2. If we put something after </body> , then that is automatically moved inside
the body , at the end, as the HTML spec requires that all content must be
inside <body> . So there may be no spaces after </body> .
In other cases everything’s straightforward – if there are spaces (just like any
character) in the document, then they become text nodes in DOM, and if we
remove them, then there won’t be any.
<!DOCTYPE HTML>
<html><head><title>About elks</title></head><body>The truth about elks.</body></h
▾ HTML
▾ HEAD
▾ TITLE
#text About elks
▾ BODY
#text The truth about elks.
Edge spaces and in-between empty text are usually hidden in tools
Browser tools (to be covered soon) that work with DOM usually do not show
spaces at the start/end of the text and empty text nodes (line-breaks) between
tags.
That’s because they are mainly used to decorate HTML, and do not affect how
it is shown (in most cases).
On further DOM pictures we’ll sometimes omit them where they are irrelevant,
to keep things short.
Autocorrection
For instance, the top tag is always <html> . Even if it doesn’t exist in the
document – it will exist in the DOM, the browser will create it. The same goes for
<body> .
As an example, if the HTML file is a single word "Hello" , the browser will wrap
it into <html> and <body> , add the required <head> , and the DOM will be:
▾ HTML
▾ HEAD
▾ BODY
#text Hello
<p>Hello
<li>Mom
<li>and
<li>Dad
…Will become a normal DOM, as the browser reads tags and restores the
missing parts:
▾ HTML
▾ HEAD
▾ BODY
▾P
#text Hello
▾ LI
#text Mom
▾ LI
#text and
▾ LI
#text Dad
<table id="table"><tr><td>1</td></tr></table>
▾ TABLE
▾ TBODY
▾ TR
▾ TD
#text 1
You see? The <tbody> appeared out of nowhere. You should keep this in
mind while working with tables to avoid surprises.
<!DOCTYPE HTML>
<html>
<body>
The truth about elks.
<ol>
<li>An elk is a smart</li>
<!-- comment -->
<li>...and cunning animal!</li>
</ol>
</body>
</html>
▾ HTML
▾ HEAD
▾ BODY
#text The truth about elks.
▾ OL
#text ↵␣␣␣␣␣␣
▾ LI
#text An elk is a smart
#text ↵␣␣␣␣␣␣
#comment comment
#text ↵␣␣␣␣␣␣
▾ LI
#text ...and cunning animal!
#text ↵␣␣␣␣
#text ↵␣␣↵
Here we see a new tree node type – comment node, labeled as #comment .
We may think – why is a comment added to the DOM? It doesn’t affect the visual
representation in any way. But there’s a rule – if something’s in HTML, then it also
must be in the DOM tree.
Everything in HTML, even comments, becomes a part of the DOM.
Even the <!DOCTYPE...> directive at the very beginning of HTML is also a
DOM node. It’s in the DOM tree right before <html> . We are not going to touch
that node, we even don’t draw it on diagrams for that reason, but it’s there.
The document object that represents the whole document is, formally, a DOM
node as well.
To see the DOM structure in real-time, try Live DOM Viewer . Just type in the
document, and it will show up DOM at an instant.
Another way to explore the DOM is to use the browser developer tools. Actually,
that’s what we use when developing.
To do so, open the web-page elks.html, turn on the browser developer tools and
switch to the Elements tab.
It should look like this:
You can see the DOM, click on elements, see their details and so on.
Please note that the DOM structure in developer tools is simplified. Text nodes are
shown just as text. And there are no “blank” (space only) text nodes at all. That’s
fine, because most of the time we are interested in element nodes.
Clicking the button in the left-upper corner allows to choose a node from the
webpage using a mouse (or other pointer devices) and “inspect” it (scroll to it in
the Elements tab). This works great when we have a huge HTML page (and
corresponding huge DOM) and would like to see the place of a particular element
in it.
Another way to do it would be just right-clicking on a webpage and selecting
“Inspect” in the context menu.
At the right part of the tools there are the following subtabs:
● Styles – we can see CSS applied to the current element rule by rule, including
built-in rules (gray). Almost everything can be edited in-place, including the
dimensions/margins/paddings of the box below.
●
Computed – to see CSS applied to the element by property: for each property
we can see a rule that gives it (including CSS inheritance and such).
●
Event Listeners – to see event listeners attached to DOM elements (we’ll
cover them in the next part of the tutorial).
● …and so on.
The best way to study them is to click around. Most values are editable in-place.
As we explore the DOM, we also may want to apply JavaScript to it. Like: get a
node and run some code to modify it, to see the result. Here are few tips to travel
between the Elements tab and the console.
●
Select the first <li> in the Elements tab.
●
Press Esc – it will open console right below the Elements tab.
From the other side, if we’re in console and have a variable referencing a DOM
node, then we can use the command inspect(node) to see it in the Elements
pane.
Or we can just output it in the console and explore “at-place”, like
document.body below:
That’s for debugging purposes of course. From the next chapter on we’ll access
and modify DOM using JavaScript.
The browser developer tools are a great help in development: we can explore the
DOM, try things and see what goes wrong.
Summary
<html> = document.documentElement
The topmost document node is document.documentElement . That’s DOM
node of <html> tag.
<body> = document.body
Another widely used DOM node is the <body> element – document.body .
<head> = document.head
The <head> tag is available as document.head .
A script cannot access an element that doesn’t exist at the moment of running.
In particular, if a script is inside <head> , then document.body is
unavailable, because the browser did not read it yet.
So, in the example below the first alert shows null :
<html>
<head>
<script>
alert( "From HEAD: " + document.body ); // null, there's no <body> yet
</script>
</head>
<body>
<script>
alert( "From BODY: " + document.body ); // HTMLBodyElement, now it exists
</script>
</body>
</html>
In the DOM, the null value means “doesn’t exist” or “no such node”.
There are two terms that we’ll use from now on:
●
Child nodes (or children) – elements that are direct children. In other words,
they are nested exactly in the given one. For instance, <head> and <body>
are children of <html> element.
●
Descendants – all elements that are nested in the given one, including
children, their children and so on.
For instance, here <body> has children <div> and <ul> (and few blank text
nodes):
<html>
<body>
<div>Begin</div>
<ul>
<li>
<b>Information</b>
</li>
</ul>
</body>
</html>
…And all descendants of <body> are not only direct children <div> , <ul> but
also more deeply nested elements, such as <li> (a child of <ul> ) and <b> (a
child of <li> ) – the entire subtree.
<html>
<body>
<div>Begin</div>
<ul>
<li>Information</li>
</ul>
<div>End</div>
<script>
for (let i = 0; i < document.body.childNodes.length; i++) {
alert( document.body.childNodes[i] ); // Text, DIV, Text, UL, ..., SCRIPT
}
</script>
...more stuff...
</body>
</html>
Please note an interesting detail here. If we run the example above, the last
element shown is <script> . In fact, the document has more stuff below, but at
the moment of the script execution the browser did not read it yet, so the script
doesn’t see it.
Properties firstChild and lastChild give fast access to the first and
last children.
They are just shorthands. If there exist child nodes, then the following is always
true:
DOM collections
As we can see, childNodes looks like an array. But actually it’s not an array,
but rather a collection – a special array-like iterable object.
There are two important consequences:
The first thing is nice. The second is tolerable, because we can use
Array.from to create a “real” array from the collection, if we want array
methods:
Changing DOM needs other methods. We will see them in the next chapter.
Please, don’t. The for..in loop iterates over all enumerable properties.
And collections have some “extra” rarely used properties that we usually do
not want to get:
<body>
<script>
// shows 0, 1, length, item, values and more.
for (let prop in document.body.childNodes) alert(prop);
</script>
</body>
Siblings are nodes that are children of the same parent. For instance, <head>
and <body> are siblings:
●
<body> is said to be the “next” or “right” sibling of <head> ,
● <head> is said to be the “previous” or “left” sibling of <body> .
For instance:
<html><head></head><body><script>
// HTML is "dense" to evade extra "blank" text nodes.
Element-only navigation
The links are similar to those given above, just with Element word inside:
● children – only those children that are element nodes.
●
firstElementChild , lastElementChild – first and last element
children.
●
previousElementSibling , nextElementSibling – neighbour
elements.
● parentElement – parent element.
This loop travels up from an arbitrary element elem to <html> , but not to
the document :
while(elem = elem.parentElement) {
alert( elem ); // parent chain till <html>
}
Let’s modify one of the examples above: replace childNodes with children .
Now it shows only elements:
<html>
<body>
<div>Begin</div>
<ul>
<li>Information</li>
</ul>
<div>End</div>
<script>
for (let elem of document.body.children) {
alert(elem); // DIV, UL, DIV, SCRIPT
}
</script>
...
</body>
</html>
<tr> :
● tr.cells – the collection of <td> and <th> cells inside the given <tr> .
● tr.sectionRowIndex – the position (index) of the given <tr> inside the
enclosing <thead>/<tbody>/<tfoot> .
● tr.rowIndex – the number of the <tr> in the table as a whole (including
all table rows).
An example of usage:
<table id="table">
<tr>
<td>one</td><td>two</td>
</tr>
<tr>
<td>three</td><td>four</td>
</tr>
</table>
<script>
// get the content of the first row, second cell
alert( table.rows[0].cells[1].innerHTML ) // "two"
</script>
There are also additional navigation properties for HTML forms. We’ll look at them
later when we start working with forms.
Summary
Given a DOM node, we can go to its immediate neighbours using navigation
properties.
There are two main sets of them:
● For all nodes: parentNode , childNodes , firstChild , lastChild ,
previousSibling , nextSibling .
●
For element nodes only: parentElement , children ,
firstElementChild , lastElementChild ,
previousElementSibling , nextElementSibling .
Some types of DOM elements, e.g. tables, provide additional properties and
collections to access their content.
✔ Tasks
DOM children
importance: 5
<html>
<body>
<div>Users:</div>
<ul>
<li>John</li>
<li>Pete</li>
</ul>
</body>
</html>
How to access:
To solution
To solution
You’ll need to get all diagonal <td> from the <table> and paint them using the
code:
To solution
document.getElementById or just id
If an element has the id attribute, then there’s a global variable by the name
from that id .
<div id="elem">
<div id="elem-content">Element</div>
</div>
<script>
alert(elem); // DOM-element with id="elem"
alert(window.elem); // accessing global variable like this also works
<div id="elem"></div>
<script>
let elem = 5;
alert(elem); // 5
</script>
For instance:
<div id="elem">
<div id="elem-content">Element</div>
</div>
<script>
let elem = document.getElementById('elem');
elem.style.background = 'red';
</script>
Here in the tutorial we’ll often use id to directly reference an element, but that’s
only to keep things short. In real life document.getElementById is the
preferred method.
If there are multiple elements with the same id , then the behavior of
corresponding methods is unpredictable. The browser may return any of them
at random. So please stick to the rule and keep id unique.
⚠ Only document.getElementById , not anyNode.getElementById
querySelectorAll
Here we look for all <li> elements that are last children:
<ul>
<li>The</li>
<li>test</li>
</ul>
<ul>
<li>has</li>
<li>passed</li>
</ul>
<script>
let elements = document.querySelectorAll('ul > li:last-child');
This method is indeed powerful, because any CSS selector can be used.
querySelector
The call to elem.querySelector(css) returns the first element for the given
CSS selector.
In other words, the result is the same as elem.querySelectorAll(css)
[0] , but the latter is looking for all elements and picking one, while
elem.querySelector just looks for one. So it’s faster and shorter to write.
matches
The method comes handy when we are iterating over elements (like in array or
something) and trying to filter those that interest us.
For instance:
<a href="http://example.com/file.zip">...</a>
<a href="http://ya.ru">...</a>
<script>
// can be any collection instead of document.body.children
for (let elem of document.body.children) {
if (elem.matches('a[href$="zip"]')) {
alert("The archive reference: " + elem.href );
}
}
</script>
closest
Ancestors of an element are: parent, the parent of parent, its parent and so on.
The ancestors together form the chain of parents from the element to the top.
The method elem.closest(css) looks the nearest ancestor that matches the
CSS-selector. The elem itself is also included in the search.
In other words, the method closest goes up from the element and checks each
of parents. If it matches the selector, then the search stops, and the ancestor is
returned.
For instance:
<h1>Contents</h1>
<div class="contents">
<ul class="book">
<li class="chapter">Chapter 1</li>
<li class="chapter">Chapter 1</li>
</ul>
</div>
<script>
let chapter = document.querySelector('.chapter'); // LI
alert(chapter.closest('.book')); // UL
alert(chapter.closest('.contents')); // DIV
getElementsBy*
There are also other methods to look for nodes by a tag, class, etc.
Today, they are mostly history, as querySelector is more powerful and shorter
to write.
So here we cover them mainly for completeness, while you can still find them in
the old scripts.
● elem.getElementsByTagName(tag) looks for elements with the given tag
and returns the collection of them. The tag parameter can also be a star "*"
for “any tags”.
●
elem.getElementsByClassName(className) returns elements that
have the given CSS class.
● document.getElementsByName(name) returns elements with the given
name attribute, document-wide. very rarely used.
For instance:
<table id="table">
<tr>
<td>Your age:</td>
<td>
<label>
<input type="radio" name="age" value="young" checked> less than 18
</label>
<label>
<input type="radio" name="age" value="mature"> from 18 to 50
</label>
<label>
<input type="radio" name="age" value="senior"> more than 60
</label>
</td>
</tr>
</table>
<script>
let inputs = table.getElementsByTagName('input');
Novice developers sometimes forget the letter "s" . That is, they try to call
getElementByTagName instead of getElementsByTagName .
// doesn't work
document.getElementsByTagName('input').value = 5;
That won’t work, because it takes a collection of inputs and assigns the value
to it rather than to elements inside it.
We should either iterate over the collection or get an element by its index, and
then assign, like this:
<script>
// find by name attribute
let form = document.getElementsByName('my-form')[0];
Live collections
1. The first one creates a reference to the collection of <div> . As of now, its
length is 1 .
2. The second scripts runs after the browser meets one more <div> , so its
length is 2 .
<div>First div</div>
<script>
let divs = document.getElementsByTagName('div');
alert(divs.length); // 1
</script>
<div>Second div</div>
<script>
alert(divs.length); // 2
</script>
<script>
let divs = document.querySelectorAll('div');
alert(divs.length); // 1
</script>
<div>Second div</div>
<script>
alert(divs.length); // 1
</script>
Now we can easily see the difference. The static collection did not increase after
the appearance of a new div in the document.
Summary
querySelector CSS-selector ✔ -
querySelectorAll CSS-selector ✔ -
getElementById id - -
getElementsByName name - ✔
getElementsByClassName class ✔ ✔
Besides that:
●
There is elem.matches(css) to check if elem matches the given CSS
selector.
●
There is elem.closest(css) to look for the nearest ancestor that matches
the given CSS-selector. The elem itself is also checked.
And let’s mention one more method here to check for the child-parent relationship,
as it’s sometimes useful:
● elemA.contains(elemB) returns true if elemB is inside elemA (a
descendant of elemA ) or when elemA==elemB .
✔ Tasks
How to find?
Open the page table.html in a separate window and make use of browser tools for
that.
To solution
In this chapter we’ll see more into what they are and their most used properties.
DOM nodes have different properties depending on their class. For instance, an
element node corresponding to tag <a> has link-related properties, and the one
corresponding to <input> has input-related properties and so on. Text nodes
are not the same as element nodes. But there are also common properties and
methods between all of them, because all classes of DOM nodes form a single
hierarchy.
Each DOM node belongs to the corresponding built-in class.
The root of the hierarchy is EventTarget , that is inherited by Node , and
other DOM nodes inherit from it.
Here’s the picture, explanations to follow:
The classes are:
● EventTarget – is the root “abstract” class. Objects of that class are never
created. It serves as a base, so that all DOM nodes support so-called “events”,
we’ll study them later.
● Node – is also an “abstract” class, serving as a base for DOM nodes. It
provides the core tree functionality: parentNode , nextSibling ,
childNodes and so on (they are getters). Objects of Node class are never
created. But there are concrete node classes that inherit from it, namely: Text
for text nodes, Element for element nodes and more exotic ones like
Comment for comment nodes.
●
Element – is a base class for DOM elements. It provides element-level
navigation like nextElementSibling , children and searching methods
like getElementsByTagName , querySelector . A browser supports not
only HTML, but also XML and SVG. The Element class serves as a base for
more specific classes: SVGElement , XMLElement and HTMLElement .
●
HTMLElement – is finally the basic class for all HTML elements. It is
inherited by various HTML elements:
●
HTMLInputElement – the class for <input> elements,
● HTMLBodyElement – the class for <body> elements,
●
HTMLAnchorElement – the class for <a> elements
● …and so on, each tag has its own class that may provide specific properties
and methods.
So, the full set of properties and methods of a given node comes as the result of
the inheritance.
For example, let’s consider the DOM object for an <input> element. It belongs
to HTMLInputElement class. It gets properties and methods as a superposition
of:
●
HTMLInputElement – this class provides input-specific properties, and
inherits from…
● HTMLElement – it provides common HTML element methods (and
getters/setters) and inherits from…
● Element – provides generic element methods and inherits from…
● Node – provides common DOM node properties and inherits from…
● EventTarget – gives the support for events (to be covered),
● …and finally it inherits from Object , so “pure object” methods like
hasOwnProperty are also available.
To see the DOM node class name, we can recall that an object usually has the
constructor property. It references to the class constructor, and
constructor.name is its name:
As we can see, DOM nodes are regular JavaScript objects. They use prototype-
based classes for inheritance.
That’s also easy to see by outputting an element with console.dir(elem) in
a browser. There in the console you can see HTMLElement.prototype ,
Element.prototype and so on.
console.dir(elem) versus console.log(elem)
Try it on document.body .
// Define HTMLInputElement
// The colon ":" means that HTMLInputElement inherits from HTMLElement
interface HTMLInputElement: HTMLElement {
// here go properties and methods of <input> elements
For instance:
<body>
<script>
let elem = document.body;
In modern scripts, we can use instanceof and other class-based tests to see
the node type, but sometimes nodeType may be simpler. We can only read
nodeType , not change it.
Given a DOM node, we can read its tag name from nodeName or tagName
properties:
For instance:
Sure, the difference is reflected in their names, but is indeed a bit subtle.
● The tagName property exists only for Element nodes.
● The nodeName is defined for any Node :
●
for elements it means the same as tagName .
●
for other node types (text, comment, etc.) it has a string with the node type.
<script>
// for comment
alert( document.body.firstChild.tagName ); // undefined (not an element)
alert( document.body.firstChild.nodeName ); // #comment
// for document
alert( document.tagName ); // undefined (not an element)
alert( document.nodeName ); // #document
</script>
</body>
If we only deal with elements, then tagName is the only thing we should use.
In XML mode the case is kept “as is”. Nowadays XML mode is rarely used.
We can also modify it. So it’s one of most powerful ways to change the page.
The example shows the contents of document.body and then replaces it
completely:
<body>
<p>A paragraph</p>
<div>A div</div>
<script>
alert( document.body.innerHTML ); // read the current contents
document.body.innerHTML = 'The new BODY!'; // replace it
</script>
</body>
We can try to insert invalid HTML, the browser will fix our errors:
<body>
<script>
document.body.innerHTML = '<b>test'; // forgot to close the tag
alert( document.body.innerHTML ); // <b>test</b> (fixed)
</script>
</body>
Like this:
elem.innerHTML += "...";
// is a shorter way to write:
elem.innerHTML = elem.innerHTML + "..."
As the content is “zeroed-out” and rewritten from the scratch, all images
and other resources will be reloaded.
In the chatDiv example above the line chatDiv.innerHTML+="How
goes?" re-creates the HTML content and reloads smile.gif (hope it’s
cached). If chatDiv has a lot of other text and images, then the reload becomes
clearly visible.
There are other side-effects as well. For instance, if the existing text was selected
with the mouse, then most browsers will remove the selection upon rewriting
innerHTML . And if there was an <input> with a text entered by the visitor,
then the text will be removed. And so on.
Luckily, there are other ways to add HTML besides innerHTML , and we’ll study
them soon.
The outerHTML property contains the full HTML of the element. That’s like
innerHTML plus the element itself.
Here’s an example:
<script>
alert(elem.outerHTML); // <div id="elem">Hello <b>World</b></div>
</script>
Beware: unlike innerHTML , writing to outerHTML does not change the
element. Instead, it replaces it as a whole in the outer context.
Yeah, sounds strange, and strange it is, that’s why we make a separate note
about it here. Take a look.
Consider the example:
<div>Hello, world!</div>
<script>
let div = document.querySelector('div');
In the line (*) we take the full HTML of <div>...</div> and replace it by
<p>...</p> . In the outer document we can see the new content instead of the
<div> . But the old div variable is still the same.
The outerHTML assignment does not modify the DOM element, but extracts it
from the outer context and inserts a new piece of HTML instead of it.
Novice developers sometimes make an error here: they modify
div.outerHTML and then continue to work with div as if it had the new
content in it.
That’s possible with innerHTML , but not with outerHTML .
We can write to outerHTML , but should keep in mind that it doesn’t change the
element we’re writing to. It creates the new content on its place instead. We can
get a reference to new elements by querying DOM.
Other node types have their counterpart: nodeValue and data properties.
These two are almost the same for practical use, there are only minor
specification differences. So we’ll use data , because it’s shorter.
For text nodes we can imagine a reason to read or modify them, but why
comments? Usually, they are not interesting at all, but sometimes developers
embed information or template instructions into HTML in them, like this:
The textContent provides access to the text inside the element: only text,
minus all <tags> .
For instance:
<div id="news">
<h1>Headline!</h1>
<p>Martians attack people!</p>
</div>
<script>
// Headline! Martians attack people!
alert(news.textContent);
</script>
As we can see, only text is returned, as if all <tags> were cut out, but the text in
them remained.
In practice, reading such text is rarely needed.
Writing to textContent is much more useful, because it allows to write
text the “safe way”.
Let’s say we have an arbitrary string, for instance entered by a user, and want to
show it.
●
With innerHTML we’ll have it inserted “as HTML”, with all HTML tags.
●
With textContent we’ll have it inserted “as text”, all symbols are treated
literally.
<div id="elem1"></div>
<div id="elem2"></div>
<script>
let name = prompt("What's your name?", "<b>Winnie-the-pooh!</b>");
elem1.innerHTML = name;
elem2.textContent = name;
</script>
1. The first <div> gets the name “as HTML”: all tags become tags, so we see
the bold name.
2. The second <div> gets the name “as text”, so we literally see <b>Winnie-
the-pooh!</b> .
In most cases, we expect the text from a user, and want to treat it as text. We
don’t want unexpected HTML in our site. An assignment to textContent does
exactly that.
The “hidden” attribute and the DOM property specifies whether the element is
visible or not.
We can use it in HTML or assign using JavaScript, like this:
<script>
elem.hidden = true;
</script>
<script>
setInterval(() => elem.hidden = !elem.hidden, 1000);
</script>
More properties
DOM elements also have additional properties, many of them provided by the
class:
● value – the value for <input> , <select> and <textarea>
( HTMLInputElement , HTMLSelectElement …).
●
href – the “href” for <a href="..."> ( HTMLAnchorElement ).
●
id – the value of “id” attribute, for all elements ( HTMLElement ).
●
…and much more…
For instance:
<script>
alert(elem.type); // "text"
alert(elem.id); // "elem"
alert(elem.value); // value
</script>
Most standard HTML attributes have the corresponding DOM property, and we
can access it like that.
If we want to know the full list of supported properties for a given class, we can
find them in the specification. For instance, HTMLInputElement is documented at
https://html.spec.whatwg.org/#htmlinputelement .
Or if we’d like to get them fast or are interested in a concrete browser specification
– we can always output the element using console.dir(elem) and read the
properties. Or explore “DOM properties” in the Elements tab of the browser
developer tools.
Summary
Each DOM node belongs to a certain class. The classes form a hierarchy. The full
set of properties and methods come as the result of inheritance.
Main DOM node properties are:
nodeType
We can use it to see if a node is a text or an element node. It has a numeric value:
1 – for elements, 3 – for text nodes, and few other for other node types. Read-
only.
nodeName/tagName
For elements, tag name (uppercased unless XML-mode). For non-element nodes
nodeName describes what it is. Read-only.
innerHTML
The HTML content of the element. Can be modified.
outerHTML
The full HTML of the element. A write operation into elem.outerHTML does not
touch elem itself. Instead it gets replaced with the new HTML in the outer
context.
nodeValue/data
The content of a non-element node (text, comment). These two are almost the
same, usually we use data . Can be modified.
textContent
The text inside the element: HTML minus all <tags> . Writing into it puts the text
inside the element, with all special characters and tags treated exactly as text.
Can safely insert user-generated text and protect from unwanted HTML insertions.
hidden
When set to true , does the same as CSS display:none .
DOM nodes also have other properties depending on their class. For instance,
<input> elements ( HTMLInputElement ) support value , type , while
<a> elements ( HTMLAnchorElement ) support href etc. Most standard
HTML attributes have a corresponding DOM property.
Although, HTML attributes and DOM properties are not always the same, as we’ll
see in the next chapter.
✔ Tasks
Count descendants
importance: 5
To solution
<html>
<body>
<script>
alert(document.body.lastChild.nodeType);
</script>
</body>
</html>
To solution
Tag in comment
importance: 3
<script>
let body = document.body;
To solution
To solution
But the attribute-property mapping is not one-to-one! In this chapter we’ll pay
attention to separate these two notions, to see how to work with them, when they
are the same, and when they are different.
DOM properties
We’ve already seen built-in DOM properties. There’s a lot. But technically no one
limits us, and if it’s not enough – we can add our own.
DOM nodes are regular JavaScript objects. We can alter them.
For instance, let’s create a new property in document.body :
document.body.myData = {
name: 'Caesar',
title: 'Imperator'
};
alert(document.body.myData.title); // Imperator
document.body.sayTagName = function() {
alert(this.tagName);
};
We can also modify built-in prototypes like Element.prototype and add new
methods to all elements:
Element.prototype.sayHi = function() {
alert(`Hello, I'm ${this.tagName}`);
};
So, DOM properties and methods behave just like those of regular JavaScript
objects:
● They can have any value.
●
They are case-sensitive (write elem.nodeType , not elem.NoDeTyPe ).
HTML attributes
In HTML, tags may have attributes. When the browser parses the HTML to create
DOM objects for tags, it recognizes standard attributes and creates DOM
properties from them.
So when an element has id or another standard attribute, the corresponding
property gets created. But that doesn’t happen if the attribute is non-standard.
For instance:
Please note that a standard attribute for one element can be unknown for another
one. For instance, "type" is standard for <input> (HTMLInputElement ),
but not for <body> (HTMLBodyElement ). Standard attributes are described in
the specification for the corresponding element class.
Here we can see it:
<body>
<div id="elem" about="Elephant"></div>
<script>
alert( elem.getAttribute('About') ); // (1) 'Elephant', reading
Please note:
1. getAttribute('About') – the first letter is uppercase here, and in HTML
it’s all lowercase. But that doesn’t matter: attribute names are case-insensitive.
2. We can assign anything to an attribute, but it becomes a string. So here we
have "123" as the value.
3. All attributes including ones that we set are visible in outerHTML .
4. The attributes collection is iterable and has all the attributes of the
element (standard and non-standard) as objects with name and value
properties.
Property-attribute synchronization
<input>
<script>
let input = document.querySelector('input');
But there are exclusions, for instance input.value synchronizes only from
attribute → to property, but not back:
<input>
<script>
let input = document.querySelector('input');
That “feature” may actually come in handy, because the user actions may lead to
value changes, and then after them, if we want to recover the “original” value
from HTML, it’s in the attribute.
<script>
alert(input.getAttribute('checked')); // the attribute value is: empty string
alert(input.checked); // the property value is: true
</script>
There are other examples. The style attribute is a string, but the style
property is an object:
<script>
// string
alert(div.getAttribute('style')); // color:red;font-size:120%
// object
alert(div.style); // [object CSSStyleDeclaration]
alert(div.style.color); // red
</script>
Here’s an example:
// property
alert(a.href ); // full URL in the form http://site.com/page#hello
</script>
If we need the value of href or any other attribute exactly as written in the
HTML, we can use getAttribute .
Non-standard attributes, dataset
When writing HTML, we use a lot of standard attributes. But what about non-
standard, custom ones? First, let’s see whether they are useful or not? What for?
Sometimes non-standard attributes are used to pass custom data from HTML to
JavaScript, or to “mark” HTML-elements for JavaScript.
Like this:
<script>
// the code finds an element with the mark and shows what's requested
let user = {
name: "Pete",
age: 25
};
<style>
/* styles rely on the custom attribute "order-state" */
.order[order-state="new"] {
color: green;
}
.order[order-state="pending"] {
color: blue;
}
.order[order-state="canceled"] {
color: red;
}
</style>
<div class="order" order-state="new">
A new order.
</div>
But there may be a possible problem with custom attributes. What if we use a
non-standard attribute for our purposes and later the standard introduces it and
makes it do something? The HTML language is alive, it grows, more attributes
appear to suit the needs of developers. There may be unexpected effects in such
case.
To avoid conflicts, there exist data-* attributes.
All attributes starting with “data-” are reserved for programmers’ use. They
are available in the dataset property.
Like this:
<body data-about="Elephants">
<script>
alert(document.body.dataset.about); // Elephants
</script>
<style>
.order[data-order-state="new"] {
color: green;
}
.order[data-order-state="pending"] {
color: blue;
}
.order[data-order-state="canceled"] {
color: red;
}
</style>
<script>
// read
alert(order.dataset.orderState); // new
// modify
order.dataset.orderState = "pending"; // (*)
</script>
Please note that we can not only read, but also modify data-attributes. Then CSS
updates the view accordingly: in the example above the last line (*) changes
the color to blue.
Summary
●
Attributes – is what’s written in HTML.
●
Properties – is what’s in DOM objects.
A small comparison:
Properties Attributes
Type Any value, standard properties have types described in the spec A string
✔ Tasks
Write the code to select the element with data-widget-name attribute from the
document and to read its value.
<!DOCTYPE html>
<html>
<body>
<script>
/* your code */
</script>
</body>
</html>
To solution
Example:
<script>
// setting style for a single link
let link = document.querySelector('a');
link.style.color = 'orange';
</script>
The list:
http://google.com
/tutorial.html
local/path
ftp://ftp.com/my.zip
http://nodejs.org
http://internal.com/test
To solution
For a start, let’s see how to add a message on the page that looks nicer than
alert .
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<div class="alert">
<strong>Hi there!</strong> You've read an important message.
</div>
That was an HTML example. Now let’s create the same div with JavaScript
(assuming that the styles are still in the HTML or an external CSS file).
Creating an element
document.createElement(tag)
Creates a new element node with the given tag:
document.createTextNode(text)
Creates a new text node with the given text:
After that, we have our DOM element ready. Right now it is just in a variable and
we cannot see it. That is because it’s not yet inserted into the page.
Insertion methods
To make the div show up, we need to insert it somewhere into document . For
instance, in document.body .
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<script>
let div = document.createElement('div');
div.className = "alert alert-success";
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";
document.body.appendChild(div);
</script>
Here’s a brief list of methods to insert a node into a parent element
( parentElem for short):
parentElem.appendChild(node)
Appends node as the last child of parentElem .
<ol id="list">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
let newLi = document.createElement('li');
newLi.innerHTML = 'Hello, world!';
list.appendChild(newLi);
</script>
parentElem.insertBefore(node, nextSibling)
Inserts node before nextSibling into parentElem .
The following code inserts a new list item before the second <li> :
<ol id="list">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
let newLi = document.createElement('li');
newLi.innerHTML = 'Hello, world!';
list.insertBefore(newLi, list.children[1]);
</script>
list.insertBefore(newLi, list.firstChild);
parentElem.replaceChild(node, oldChild)
Replaces oldChild with node among children of parentElem .
prepend/append/before/after
This set of methods provides more flexible insertions:
● node.append(...nodes or strings) – append nodes or strings at the
end of node ,
●
node.prepend(...nodes or strings) – insert nodes or strings into the
beginning of node ,
●
node.before(...nodes or strings) –- insert nodes or strings before
the node ,
● node.after(...nodes or strings) –- insert nodes or strings after the
node ,
●
node.replaceWith(...nodes or strings) –- replaces node with the
given nodes or strings.
All of them accept a list of DOM nodes and/or text strings. If a string is given it’s
inserted as a text node.
Here’s an example of using these methods to add more items to a list and the text
before/after it:
<ol id="ol">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
ol.before('before');
ol.after('after');
before
1. prepend
2. 0
3. 1
4. 2
5. append
after
before
<ol id="ol">
<li>prepend</li>
<li>0</li>
<li>1</li>
<li>2</li>
<li>append</li>
</ol>
after
These methods can insert multiple lists of nodes and text pieces in a single call.
For instance, here a string and an element are inserted:
<div id="div"></div>
<script>
div.before('<p>Hello</p>', document.createElement('hr'));
</script>
<p>Hello</p>
<hr>
<div id="div"></div>
insertAdjacentHTML/Text/Element
There’s another, pretty versatile method:
elem.insertAdjacentHTML(where, html) .
The first parameter is a code word, specifying where to insert relative to elem .
Must be one of the following:
●
"beforebegin" – insert html immediately before elem ,
●
"afterbegin" – insert html into elem , at the beginning,
●
"beforeend" – insert html into elem , at the end,
●
"afterend" – insert html immediately after elem .
<div id="div"></div>
<script>
div.insertAdjacentHTML('beforebegin', '<p>Hello</p>');
div.insertAdjacentHTML('afterend', '<p>Bye</p>');
</script>
We can easily notice similarities between this and the previous picture. The
insertion points are actually the same, but this method inserts HTML.
The method has two brothers:
● elem.insertAdjacentText(where, text) – the same syntax, but a
string of text is inserted “as text” instead of HTML,
●
elem.insertAdjacentElement(where, elem) – the same syntax, but
inserts an element.
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<script>
document.body.insertAdjacentHTML("afterbegin", `<div class="alert alert-success
<strong>Hi there!</strong> You've read an important message.
</div>`);
</script>
Sometimes when we have a big element, that may be faster and simpler.
●
The call elem.cloneNode(true) creates a “deep” clone of the element –
with all attributes and subelements. If we call elem.cloneNode(false) ,
then the clone is made without child elements.
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<script>
let div2 = div.cloneNode(true); // clone the message
div2.querySelector('strong').innerHTML = 'Bye there!'; // change the clone
DocumentFragment
<ul id="ul"></ul>
<script>
function getListContent() {
let fragment = new DocumentFragment();
return fragment;
}
ul.append(getListContent()); // (*)
</script>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<ul id="ul"></ul>
<script>
function getListContent() {
let result = [];
Removal methods
parentElem.removeChild(node)
Removes node from parentElem (assuming it’s a child).
node.remove()
Removes the node from its place.
We can easily see that the second method is much shorter. The first one exists for
historical reasons.
Please note:
If we want to move an element to another place – there’s no need to remove it
from the old one.
All insertion methods automatically remove the node from the old place.
For instance, let’s swap elements:
<div id="first">First</div>
<div id="second">Second</div>
<script>
// no need to call remove
second.after(first); // take #second and after it - insert #first
</script>
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<script>
let div = document.createElement('div');
div.className = "alert alert-success";
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";
document.body.append(div);
setTimeout(() => div.remove(), 1000);
// or setTimeout(() => document.body.removeChild(div), 1000);
</script>
The syntax:
The call to document.write(html) writes the html into page “right here
and now”. The html string can be dynamically generated, so it’s kind of flexible.
We can use JavaScript to create a full-fledged webpage and write it.
The method comes from times when there was no DOM, no standards… Really
old times. It still lives, because there are scripts using it.
In modern scripts we can rarely see it, because of the following important
limitation:
The call to document.write only works while the page is loading.
So it’s kind of unusable at “after loaded” stage, unlike other DOM methods we
covered above.
That was the downside.
Technically, when document.write is called while the browser is reading
(“parsing”) incoming HTML, and it writes something, the browser consumes it just
as it were initially there, in the HTML text.
That gives us the upside – it works blazingly fast, because there’s no DOM
modification. It writes directly into the page text, while the DOM is not yet built,
and the browser puts it into DOM at generation-time.
So if we need to add a lot of text into HTML dynamically, and we’re at page
loading phase, and the speed matters, it may help. But in practice these
requirements rarely come together. And usually we can see this method in scripts
just because they are old.
Summary
After the page is loaded such a call erases the document. Mostly seen in old
scripts.
✔ Tasks
1. elem.append(document.createTextNode(text))
2. elem.innerHTML = text
3. elem.textContent = text
To solution
Clear the element
importance: 5
<ol id="elem">
<li>Hello</li>
<li>World</li>
</ol>
<script>
function clear(elem) { /* your code */ }
To solution
Run the example. Why does table.remove() not delete the text "aaa" ?
<table id="table">
aaa
<tr>
<td>Test</td>
</tr>
</table>
<script>
alert(table); // the table, as it should be
table.remove();
// why there's still aaa in the document?
</script>
To solution
Create a list
importance: 4
To solution
Write a function createTree that creates a nested ul/li list from the nested
object.
For instance:
let data = {
"Fish": {
"trout": {},
"salmon": {}
},
"Tree": {
"Huge": {
"sequoia": {},
"oak": {}
},
"Flowering": {
"apple tree": {},
"magnolia": {}
}
}
};
The syntax:
Fish
trout
salmon
Tree
Huge
sequoia
oak
Flowering
apple tree
magnolia
1. Create the HTML for the tree and then assign to container.innerHTML .
2. Create tree nodes and append with DOM methods.
P.S. The tree should not have “extra” elements like empty <ul></ul> for the
leaves.
To solution
Write the code that adds to each <li> the number of its descendants. Skip
leaves (nodes without children).
The result:
Animals [9]
Mammals [4]
Cows
Donkeys
Dogs
Tigers
Other [3]
Snakes
Birds
Lizards
Fishes [5]
Aquarium [2]
Guppy
Angelfish
Sea [1]
Sea trout
To solution
Create a calendar
importance: 4
The call should create a calendar for the given year/month and put it inside
elem .
The calendar should be a table, where a week is <tr> , and a day is <td> . The
table top should be <th> with weekday names: the first day should be Monday,
and so on till Sunday.
MO TU WE TH FR SA SU
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
P.S. For this task it’s enough to generate the calendar, should not yet be clickable.
Open a sandbox for the task.
To solution
19:13:05
Start Stop
Use HTML/CSS for the styling, JavaScript only updates time in elements.
To solution
<ul id="ul">
<li id="one">1</li>
<li id="two">4</li>
</ul>
To solution
There’s a table:
John Smith 10
Pete Brown 15
Ann Lee 5
To solution
CSS is always the preferred way – not only for HTML, but in JavaScript as well.
We should only manipulate the style property if classes “can’t handle it”.
For other cases, like making the text red, adding a background icon – describe
that in CSS and then apply the class. That’s more flexible and easier to support.
For instance:
So we can operate both on the full class string using className or on individual
classes using classList . What we choose depends on our needs.
Methods of classList :
● elem.classList.add/remove("class") – adds/removes the class.
● elem.classList.toggle("class") – adds the class if it doesn’t exist,
otherwise removes it.
●
elem.classList.contains("class") – returns true/false , checks
for the given class.
Besides, classList is iterable, so we can list all classes with for..of , like
this:
Element style
For instance:
Prefixed properties
Browser-prefixed properties like -moz-border-radius , -webkit-
border-radius also follow the same rule, for instance:
button.style.MozBorderRadius = '5px';
button.style.WebkitBorderRadius = '5px';
If we set display to an empty string, then the browser applies CSS classes and
its built-in styles normally, as if there were no such display property at all.
<div id="div">Button</div>
<script>
// we can set special style flags like "important" here
div.style.cssText=`color: red !important;
background-color: yellow;
width: 100px;
text-align: center;
`;
alert(div.style.cssText);
</script>
This property is rarely used, because such assignment removes all existing
styles: it does not add, but replaces them. May occasionally delete something
needed. But we can safely use it for new elements, when we know we won’t
delete an existing style.
The same can be accomplished by setting an attribute:
div.setAttribute('style', 'color: red...') .
<body>
<script>
// doesn't work!
document.body.style.margin = 20;
alert(document.body.style.margin); // '' (empty string, the assignment is ign
alert(document.body.style.marginTop); // 20px
alert(document.body.style.marginLeft); // 20px
</script>
</body>
Please note how the browser “unpacks” the property style.margin in the last
lines and infers style.marginLeft and style.marginTop (and other
partial margins) from it.
<head>
<style> body { color: red; margin: 5px } </style>
</head>
<body>
…But what if we need, say, to increase the margin by 20px? We would want the
current value of it.
There’s another method for that: getComputedStyle .
getComputedStyle(element[, pseudo])
element
Element to read the value for.
pseudo
A pseudo-element if required, for instance ::before . An empty string or no
argument means the element itself.
The result is an object with style properties, like elem.style , but now with
respect to all CSS classes.
For instance:
<head>
<style> body { color: red; margin: 5px } </style>
</head>
<body>
<script>
let computedStyle = getComputedStyle(document.body);
</body>
Computed and resolved values
There are two concepts in CSS :
1. A computed style value is the value after all CSS rules and CSS inheritance
is applied, as the result of the CSS cascade. It can look like height:1em
or font-size:125% .
2. A resolved style value is the one finally applied to the element. Values like
1em or 125% are relative. The browser takes the computed value and
makes all units fixed and absolute, for instance: height:20px or font-
size:16px . For geometry properties resolved values may have a floating
point, like width:50.5px .
<style>
body {
margin: 10px;
}
</style>
<script>
let style = getComputedStyle(document.body);
alert(style.margin); // empty string in Firefox
</script>
“Visited” links styles are hidden!
Visited links may be colored using :visited CSS pseudoclass.
JavaScript may not see the styles applied by :visited . And also, there’s a
limitation in CSS that forbids to apply geometry-changing styles in
:visited . That’s to guarantee that there’s no sideway for an evil page to
test if a link was visited and hence to break the privacy.
Summary
To read the resolved styles (with respect to all classes, after all CSS is applied
and final values are calculated):
●
The getComputedStyle(elem[, pseudo]) returns the style-like object
with them. Read-only.
✔ Tasks
Create a notification
importance: 5
// shows an element with the text "Hello" near the right-top of the window
showNotification({
top: 10, // 10px from the top of the window (by default 0px)
right: 10, // 10px from the right edge of the window (by default 0px)
html: "Hello!", // the HTML of notification
className: "welcome" // an additional class for the div (optional)
});
Use CSS positioning to show the element at given top/right coordinates. The
source document has the necessary styles.
To solution
Sample element
As a sample element to demonstrate properties we’ll use the one given below:
<div id="example">
...Text...
</div>
<style>
#example {
width: 300px;
height: 200px;
border: 25px solid #E8C48F;
padding: 20px;
overflow: auto;
}
</style>
It has the border, padding and scrolling. The full set of features. There are no
margins, as they are not the part of the element itself, and there are no special
properties for them.
The element looks like this:
Usually paddings are shown empty on illustrations, but if there’s a lot of text in
the element and it overflows, then browsers show the “overflowing” text at
padding-bottom .
Geometry
Values of these properties are technically numbers, but these numbers are “of
pixels”, so these are pixel measurements.
They are many properties, it’s difficult to fit them all in the single picture, but their
values are simple and easy to understand.
Let’s start exploring them from the outside of the element.
offsetParent, offsetLeft/Top
These properties are rarely needed, but still they are the “most outer” geometry
properties, so we’ll start with them.
The offsetParent is the nearest ancestor, that browser uses for calculating
coordinates during rendering.
That’s the nearest ancestor, that satisfies following conditions:
1. CSS-positioned ( position is absolute , relative , fixed or
sticky ),
2. or <td> , <th> , <table> ,
3. or <body> .
offsetWidth/Height
function isHidden(elem) {
return !elem.offsetWidth && !elem.offsetHeight;
}
Please note that such isHidden returns true for elements that are on-
screen, but have zero sizes (like an empty <div> ).
clientTop/Left
In our example:
●
clientLeft = 25 – left border width
● clientTop = 25 – top border width
…But to be precise – these properties are not border width/height, but rather
relative coordinates of the inner side from the outer side.
What’s the difference?
It becomes visible when the document is right-to-left (the operating system is in
Arabic or Hebrew languages). The scrollbar is then not on the right, but on the left,
and then clientLeft also includes the scrollbar width.
In that case, clientLeft would be not 25 , but with the scrollbar width 25 +
16 = 41 :
clientWidth/Height
These properties provide the size of the area inside the element borders.
They include the content width together with paddings, but without the scrollbar:
On the picture above let’s first consider clientHeight : it’s easier to evaluate.
There’s no horizontal scrollbar, so it’s exactly the sum of what’s inside the borders:
CSS-height 200px plus top and bottom paddings ( 2 * 20px ) total 240px .
Now clientWidth – here the content width is not 300px , but 284px ,
because 16px are occupied by the scrollbar. So the sum is 284px plus left and
right paddings, total 324px .
scrollWidth/Height
● Properties clientWidth/clientHeight only account for the visible part of
the element.
●
Properties scrollWidth/scrollHeight also include the scrolled out
(hidden) parts:
On the picture above:
●
scrollHeight = 723 – is the full inner height of the content area including
the scrolled out parts.
● scrollWidth = 324 – is the full inner width, here we have no horizontal
scroll, so it equals clientWidth .
We can use these properties to expand the element wide to its full width/height.
Like this:
scrollLeft/scrollTop
We’ve just covered geometry properties of DOM elements, that can be used to get
widths, heights and calculate distances.
But as we know from the chapter Styles and classes, we can read CSS-height
and width using getComputedStyle .
So why not to read the width of an element with getComputedStyle , like this?
Why should we use geometry properties instead? There are two reasons:
1. First, CSS width/height depend on another property: box-sizing that
defines “what is” CSS width and height. A change in box-sizing for CSS
purposes may break such JavaScript.
2. Second, CSS width/height may be auto , for instance for an inline
element:
<span id="elem">Hello!</span>
<script>
alert( getComputedStyle(elem).width ); // auto
</script>
And there’s one more reason: a scrollbar. Sometimes the code that works fine
without a scrollbar starts to bug with it, because a scrollbar takes the space from
the content in some browsers. So the real width available for the content is less
than CSS width. And clientWidth/clientHeight take that into account.
Summary
Elements have the following geometry properties:
● offsetParent – is the nearest positioned ancestor or td , th , table ,
body .
● offsetLeft/offsetTop – coordinates relative to the upper-left edge of
offsetParent .
●
offsetWidth/offsetHeight – “outer” width/height of an element
including borders.
● clientLeft/clientTop – the distance from the upper-left outer corner to
its upper-left inner corner. For left-to-right OS they are always the widths of
left/top borders. For right-to-left OS the vertical scrollbar is on the left so
clientLeft includes its width too.
● clientWidth/clientHeight – the width/height of the content including
paddings, but without the scrollbar.
●
scrollWidth/scrollHeight – the width/height of the content, just like
clientWidth/clientHeight , but also include scrolled-out, invisible part
of the element.
● scrollLeft/scrollTop – width/height of the scrolled out upper part of the
element, starting from its upper-left corner.
✔ Tasks
The elem.scrollTop property is the size of the scrolled out part from the top.
How to get “ scrollBottom ” – the size from the bottom?
P.S. Please check your code: if there’s no scroll or the element is fully scrolled
down, then it should return 0 .
To solution
For Windows it usually varies between 12px and 20px . If the browser doesn’t
reserve any space for it (the scrollbar is half-translucent over the text, also
happens), then it may be 0px .
P.S. The code should work for any HTML document, do not depend on its content.
To solution
.........................
.........................
.........................
.........................
.........................
.........................
.........................
Calculate them and use to place the ball into the center of the field:
.........................
.........................
.........................
.........................
.........................
.........................
.........................
P.S. Sure, centering could be done with CSS, but here we want exactly
JavaScript. Further we’ll meet other topics and more complex situations when
JavaScript must be used. Here we do a “warm-up”.
Open a sandbox for the task.
To solution
To solution
Properties clientWidth/clientHeight of
document.documentElement is exactly what we want here:
⚠ Not window.innerWidth/Height
Browsers also support properties window.innerWidth/innerHeight .
They look like what we want. So why not to use them instead?
If there exists a scrollbar, and it occupies some space,
clientWidth/clientHeight provide the width/height without it (subtract
it). In other words, they return width/height of the visible part of the document,
available for the content.
…And window.innerWidth/innerHeight ignore the scrollbar.
If there’s a scrollbar, and it occupies some space, then these two lines show
different values:
⚠ DOCTYPE is important
Please note: top-level geometry properties may work a little bit differently
when there’s no <!DOCTYPE HTML> in HTML. Odd things are possible.
These properties work well for regular elements. But for the whole page these
properties do not work as intended. In Chrome/Safari/Opera if there’s no scroll,
then documentElement.scrollHeight may be even less than
documentElement.clientHeight ! Sounds like a nonsense, weird, right?
To reliably obtain the full document height, we should take the maximum of these
properties:
let scrollHeight = Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight
);
Why so? Better don’t ask. These inconsistencies come from ancient times, not a
“smart” logic.
Luckily, we don’t have to remember these peculiarities at all, because the scroll is
available in the special properties window.pageXOffset/pageYOffset :
⚠ Important:
To scroll the page from JavaScript, its DOM must be fully built.
For instance, if we try to scroll the page from the script in <head> , it won’t
work.
scrollIntoView
The drawback of the method is that the scrollbar disappears. If it occupied some
space, then that space is now free, and the content “jumps” to fill it.
That looks a bit odd, but can be worked around if we compare clientWidth
before and after the freeze, and if it increased (the scrollbar disappeared) then
add padding to document.body in place of the scrollbar, to keep the content
width the same.
Summary
Geometry:
● Width/height of the visible part of the document (content area width/height):
document.documentElement.clientWidth/Height
● Width/height of the whole document, with the scrolled out part:
Scrolling:
●
Read the current scroll: window.pageYOffset/pageXOffset .
●
Change the current scroll:
●
window.scrollTo(pageX,pageY) – absolute coordinates,
● window.scrollBy(x,y) – scroll relative the current place,
● elem.scrollIntoView(top) – scroll to make elem visible (align with
the top/bottom of the window).
Coordinates
To move elements around we should be familiar with coordinates.
Most JavaScript methods deal with one of two coordinate systems:
1. Relative to the window(or another viewport) top/left.
2. Relative to the document top/left.
Like this:
Window coordinates do not take the scrolled out part of the document into
account, they are calculated from the window’s upper-left corner.
In other words, when we scroll the page, the element goes up or down, its window
coordinates change. That’s very important.
Also:
●
Coordinates may be decimal fractions. That’s normal, internally browser uses
them for calculations. We don’t have to round them when setting to
style.position.left/top , the browser is fine with fractions.
● Coordinates may be negative. For instance, if the page is scrolled down and
the top elem is now above the window. Then,
elem.getBoundingClientRect().top is negative.
● Some browsers (like Chrome) provide additional properties, width and
height of the element that invoked the method to
getBoundingClientRect as the result. We can also get them by
subtraction: height=bottom-top , width=right-left .
If we just look at the picture above, we can see that in JavaScript it is not so.
All window coordinates are counted from the upper-left corner, including these
ones.
elementFromPoint(x, y)
For instance, the code below highlights and outputs the tag of the element that is
now in the middle of the window:
elem.style.background = "red";
alert(elem.tagName);
In most cases such behavior is not a problem, but we should keep that in
mind.
Here’s a typical error that may occur if we don’t check for it:
message.innerHTML = html;
return message;
}
// Usage:
// add it for 5 seconds in the document
let message = createMessageUnder(elem, 'Hello, world!');
document.body.append(message);
setTimeout(() => message.remove(), 5000);
The code can be modified to show the message at the left, right, below, apply
CSS animations to “fade it in” and so on. That’s easy, as we have all the
coordinates and sizes of the element.
But note the important detail: when the page is scrolled, the message flows away
from the button.
The reason is obvious: the message element relies on position:fixed , so it
remains at the same place of the window while the page scrolls away.
To change that, we need to use document-based coordinates and
position:absolute .
Document coordinates
When the page is not scrolled, then window coordinate and document coordinates
are actually the same. Their zero points match too:
And if we scroll it, then (clientX,clientY) change, because they are relative
to the window, but (pageX,pageY) remain the same.
return {
top: box.top + pageYOffset,
left: box.left + pageXOffset
};
}
Summary
Both coordinate systems have their “pro” and “contra”, there are times we need
one or the other one, just like CSS position absolute and fixed .
✔ Tasks
In the iframe below you can see a document with the green “field”.
The coordinates that you calculate should be the same as those returned by the
mouse click.
P.S. The code should also work if the element has another size or border, not
bound to any fixed values.
To solution
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad
incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim
nisi rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.
note above
Teacher: That's nice. Were you helping him look for it?
Student: No. I was standing on it.
note below
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad
incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim
nisi rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.
To solution
Modify the solution of the previous task so that the note uses
position:absolute instead of position:fixed .
That will prevent its “runaway” from the element when the page scrolls.
Take the solution of that task as a starting point. To test the scroll, add the style
<body style="height: 2000px"> .
To solution
For instance:
The result:
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad
incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim
nisi rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.
note top-out
“
note top-in
Teacher: Why are you late?
Student: There was a man who lost a hundred dollar bill.
note right-out
Teacher: That's nice. Were you helping him look for it?
Student: No. I was standing on it.
note bottom-in
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad
incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim
nisi rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.
As the source code, take the solution of the task Show a note near the element
(absolute).
To solution
Introduction to Events
An introduction to browser events, event properties and handling patterns.
Keyboard events:
●
keydown and keyup – when the visitor presses and then releases the
button.
Document events:
●
DOMContentLoaded – when the HTML is loaded and processed, DOM is
fully built.
CSS events:
●
transitionend – when a CSS-animation finishes.
There are many other events. We’ll get into more details of particular events in
next chapters.
Event handlers
To react on events we can assign a handler – a function that runs in case of an
event.
HTML-attribute
A handler can be set in HTML with an attribute named on<event> .
For instance, to assign a click handler for an input , we can use onclick ,
like here:
Please note that inside onclick we use single quotes, because the attribute
itself is in double quotes. If we forget that the code is inside the attribute and use
double quotes inside, like this: onclick="alert("Click!")" , then it won’t
work right.
An HTML-attribute is not a convenient place to write a lot of code, so we’d better
create a JavaScript function and call it there.
Here a click runs the function countRabbits() :
<script>
function countRabbits() {
for(let i=1; i<=3; i++) {
alert("Rabbit number " + i);
}
}
</script>
Count rabbits!
DOM property
We can assign a handler using a DOM property on<event> .
Click me
If the handler is assigned using an HTML-attribute then the browser reads it,
creates a new function from the attribute content and writes it to the DOM
property.
So this way is actually the same as the previous one.
The handler is always in the DOM property: the HTML-attribute is just one of
the ways to initialize it.
Button
2. HTML + JS:
Button
As there’s only one onclick property, we can’t assign more than one
event handler.
In the example below adding a handler with JavaScript overwrites the existing
handler:
Click me
function sayThanks() {
alert('Thanks!');
}
elem.onclick = sayThanks;
The value of this inside a handler is the element. The one which has the
handler on it.
In the code below button shows its contents using this.innerHTML :
Click me
Possible mistakes
// right
button.onclick = sayThanks;
// wrong
button.onclick = sayThanks();
The difference is easy to explain. When the browser reads the attribute, it creates
a handler function with the body from its content.
So the last example is the same as:
button.onclick = function() {
sayThanks(); // the attribute content
};
addEventListener
event
Event name, e.g. "click" .
handler
The handler function.
options
An additional optional object with properties:
●
once : if true , then the listener is automatically removed after it triggers.
● capture : the phase where to handle the event, to be covered later in the
chapter Bubbling and capturing. For historical reasons, options can also be
false/true , that’s the same as {capture: false/true} .
● passive : if true , then the handler will not preventDefault() , we’ll
cover that later in Browser default actions.
function handler() {
alert( 'Thanks!' );
}
input.addEventListener("click", handler);
// ....
input.removeEventListener("click", handler);
Please note – if we don’t store the function in a variable, then we can’t remove
it. There’s no way to “read back” handlers assigned by
addEventListener .
<script>
function handler1() {
alert('Thanks!');
};
function handler2() {
alert('Thanks again!');
}
There exist events that can’t be assigned via a DOM-property. Must use
addEventListener .
<style>
input {
transition: width 1s;
width: 100px;
}
.wide {
width: 300px;
}
</style>
<script>
elem.ontransitionend = function() {
alert("DOM property"); // doesn't work
};
elem.addEventListener("transitionend", function() {
alert("addEventListener"); // shows up when the animation finishes
});
</script>
Event object
To properly handle an event we’d want to know more about what’s happened. Not
just a “click” or a “keypress”, but what were the pointer coordinates? Which key
was pressed? And so on.
When an event happens, the browser creates an event object, puts details into it
and passes it as an argument to the handler.
Here’s an example of getting mouse coordinates from the event object:
<script>
elem.onclick = function(event) {
// show event type, element and coordinates of the click
alert(event.type + " at " + event.currentTarget);
alert("Coordinates: " + event.clientX + ":" + event.clientY);
};
</script>
event.type
Event type, here it’s "click" .
event.currentTarget
Element that handled the event. That’s exactly the same as this , unless the
handler is an arrow function, or its this is bound to something else, then
event.currentTarget becomes useful.
event.clientX / event.clientY
Window-relative coordinates of the cursor, for mouse events.
There are more properties. They depend on the event type, so we’ll study them
later when we come to different events in details.
The event object is also accessible from HTML
If we assign a handler in HTML, we can also use the event object, like this:
Event type
That’s possible because when the browser reads the attribute, it creates a
handler like this: function(event) { alert(event.type) } . That is:
its first argument is called "event" , and the body is taken from the attribute.
For instance:
<script>
elem.addEventListener('click', {
handleEvent(event) {
alert(event.type + " at " + event.currentTarget);
}
});
</script>
<script>
class Menu {
handleEvent(event) {
switch(event.type) {
case 'mousedown':
elem.innerHTML = "Mouse button pressed";
break;
case 'mouseup':
elem.innerHTML += "...and released.";
break;
}
}
}
Here the same object handles both events. Please note that we need to explicitly
setup the events to listen using addEventListener . The menu object only
gets mousedown and mouseup here, not any other types of events.
The method handleEvent does not have to do all the job by itself. It can call
other event-specific methods instead, like this:
<script>
class Menu {
handleEvent(event) {
// mousedown -> onMousedown
let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
this[method](event);
}
onMousedown() {
elem.innerHTML = "Mouse button pressed";
}
onMouseup() {
elem.innerHTML += "...and released.";
}
}
Now event handlers are clearly separated, that may be easier to support.
Summary
There are 3 ways to assign event handlers:
1. HTML attribute: onclick="..." .
2. DOM property: elem.onclick = function .
3. Methods: elem.addEventListener(event, handler[, phase]) to
add, removeEventListener to remove.
HTML attributes are used sparingly, because JavaScript in the middle of an HTML
tag looks a little bit odd and alien. Also can’t write lots of code in there.
DOM properties are ok to use, but we can’t assign more than one handler of the
particular event. In many cases that limitation is not pressing.
The last way is the most flexible, but it is also the longest to write. There are few
events that only work with it, for instance transtionend and
DOMContentLoaded (to be covered). Also addEventListener supports
objects as event handlers. In that case the method handleEvent is called in
case of the event.
No matter how you assign the handler – it gets an event object as the first
argument. That object contains the details about what’s happened.
We’ll learn more about events in general and about different types of events in the
next chapters.
✔ Tasks
Hide on click
importance: 5
The demo:
To solution
Hide self
importance: 5
To solution
Which handlers run on click after the following code? Which alerts show up?
To solution
Requirements:
● The ball center should come exactly under the pointer on click (if possible
without crossing the field edge).
● CSS-animation is welcome.
● The ball must not cross field boundaries.
● When the page is scrolled, nothing should break.
Notes:
● The code should also work with different ball and field sizes, not be bound to
any fixed values.
● Use properties event.clientX/event.clientY for click coordinates.
To solution
To solution
Use JavaScript to add a closing button to the right-upper corner of each message.
Donkey [x]
The donkey or ass (Equus africanus asinus) is a domesticated
member of the horse family, Equidae. The wild ancestor of the
donkey is the African wild ass, E. africanus. The donkey has been
used as a working animal for at least 5000 years.
[x]
Cat
The domestic cat (Latin: Felis catus) is a small, typically furry,
carnivorous mammal. They are often called house cats when kept as
indoor pets or simply cats when there is no need to distinguish them
from other felids and felines. Cats are often valued by humans for
companionship and for their ability to hunt vermin.
To solution
Carousel
importance: 4
⇦ ⇨
Later we can add more features to it: infinite scrolling, dynamic loading etc.
P.S. For this task HTML/CSS structure is actually 90% of the solution.
Open a sandbox for the task.
To solution
Isn’t it a bit strange? Why does the handler on <div> run if the actual click was
on <em> ?
Bubbling
When an event happens on an element, it first runs the handlers on it, then
on its parent, then all the way up on other ancestors.
Let’s say we have 3 nested elements FORM > DIV > P with a handler on each
of them:
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
FORM
DIV
P
1. On that <p> .
2. Then on the outer <div> .
3. Then on the outer <form> .
4. And so on upwards till the document object.
The process is called “bubbling”, because events “bubble” from the inner element
up through parents like a bubble in the water.
event.target
A handler on a parent element can always get the details about where it actually
happened.
The most deeply nested element that caused the event is called a target
element, accessible as event.target .
For instance, if we have a single handler form.onclick , then it can “catch” all
clicks inside the form. No matter where the click happened, it bubbles up to
<form> and runs the handler.
In form.onclick handler:
●
this ( =event.currentTarget ) is the <form> element, because the
handler runs on it.
● event.target is the concrete element inside the form that actually was
clicked.
Check it out:
https://plnkr.co/edit/iaPo3qfpDpHzXju0Jita?p=preview
It’s possible that event.target equals this – when the click is made directly
on the <form> element.
Stopping bubbling
A bubbling event goes from the target element straight up. Normally it goes
upwards till <html> , and then to document object, and some events even
reach window , calling all handlers on the path.
But any handler may decide that the event has been fully processed and stop the
bubbling.
The method for it is event.stopPropagation() .
Click me
event.stopImmediatePropagation()
If an element has multiple event handlers on a single event, then even if one
of them stops the bubbling, the other ones still execute.
In other words, event.stopPropagation() stops the move upwards, but
on the current element all other handlers will run.
To stop the bubbling and prevent handlers on the current element from
running, there’s a method event.stopImmediatePropagation() . After
it no other handlers execute.
There’s usually no real need to prevent the bubbling. A task that seemingly
requires that may be solved by other means. One of them is to use custom
events, we’ll cover them later. Also we can write our data into the event
object in one handler and read it in another one, so we can pass to handlers
on parents information about the processing below.
Capturing
Here’s the picture of a click on <td> inside a table, taken from the specification:
That is: for a click on <td> the event first goes through the ancestors chain down
to the element (capturing phase), then it reaches the target and triggers there
(target phase), and then it goes up (bubbling phase), calling handlers on its way.
Before we only talked about bubbling, because the capturing phase is rarely
used. Normally it is invisible to us.
Handlers added using on<event> -property or using HTML attributes or using
addEventListener(event, handler) don’t know anything about
capturing, they only run on the 2nd and 3rd phases.
To catch an event on the capturing phase, we need to set the handler capture
option to true :
elem.addEventListener(..., {capture: true})
// or, just "true" is an alias to {capture: true}
elem.addEventListener(..., true)
Note that while formally there are 3 phases, the 2nd phase (“target phase”: the
event reached the element) is not handled separately: handlers on both capturing
and bubbling phases trigger at that phase.
Let’s see both capturing and bubbling in action:
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>
<form>FORM
<div>DIV
<p>P</p>
</div>
</form>
<script>
for(let elem of document.querySelectorAll('*')) {
elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true
elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
}
</script>
FORM
DIV
P
The code sets click handlers on every element in the document to see which ones
are working.
If you click on <p> , then the sequence is:
1. HTML → BODY → FORM → DIV (capturing phase, the first listener):
2. P (target phrase, triggers two times, as we’ve set two listeners: capturing and
bubbling)
3. DIV → FORM → BODY → HTML (bubbling phase, the second listener).
Listeners on same element and same phase run in their set order
If we have multiple event handlers on the same phase, assigned to the same
element with addEventListener , they run in the same order as they are
created:
Summary
When an event happens – the most nested element where it happens gets
labeled as the “target element” ( event.target ).
●
Then the event moves down from the document root to event.target ,
calling handlers assigned with addEventListener(...., true) on the
way ( true is a shorthand for {capture: true} ).
● Then handlers are called on the target element itself.
●
Then the event bubbles up from event.target up to the root, calling
handlers assigned using on<event> and addEventListener without the
3rd argument or with the 3rd argument false/{capture:false} .
Event delegation
Capturing and bubbling allow us to implement one of most powerful event
handling patterns called event delegation.
The idea is that if we have a lot of elements handled in a similar way, then instead
of assigning a handler to each of them – we put a single handler on their common
ancestor.
In the handler we get event.target , see where the event actually happened
and handle it.
Let’s see an example – the Ba-Gua diagram reflecting the ancient Chinese
philosophy.
Here it is:
Bagua Chart: Direction, Element, Color, Meaning
Northwest North Northeast
Metal Water Earth
Silver Blue Yellow
Elders Change Direction
<table>
<tr>
<th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
</tr>
<tr>
<td>...<strong>Northwest</strong>...</td>
<td>...</td>
<td>...</td>
</tr>
<tr>...2 more lines of this kind...</tr>
<tr>...2 more lines of this kind...</tr>
</table>
The table has 9 cells, but there could be 99 or 9999, doesn’t matter.
Our task is to highlight a cell <td> on click.
It will use event.target to get the clicked element and highlight it.
The code:
let selectedTd;
table.onclick = function(event) {
let target = event.target; // where was the click?
if (target.tagName != 'TD') return; // not on TD? Then we're not interested
highlight(target); // highlight it
};
function highlight(td) {
if (selectedTd) { // remove the existing highlight if any
selectedTd.classList.remove('highlight');
}
selectedTd = td;
selectedTd.classList.add('highlight'); // highlight the new td
}
Such a code doesn’t care how many cells there are in the table. We can
add/remove <td> dynamically at any time and the highlighting will still work.
In our case if we take a look inside the HTML, we can see nested tags inside
<td> , like <strong> :
<td>
<strong>Northwest</strong>
...
</td>
highlight(td); // (4)
};
Explanations:
1. The method elem.closest(selector) returns the nearest ancestor that
matches the selector. In our case we look for <td> on the way up from the
source element.
2. If event.target is not inside any <td> , then the call returns null , and
we don’t have to do anything.
3. In case of nested tables, event.target may be a <td> lying outside of the
current table. So we check if that’s actually our table’s <td> .
4. And, if it’s so, then highlight it.
The event delegation may be used to optimize event handling. We use a single
handler for similar actions on many elements. Like we did it for highlighting <td> .
But we can also use a single handler as an entry point for many different things.
For instance, we want to make a menu with buttons “Save”, “Load”, “Search” and
so on. And there’s an object with methods save , load , search ….
The first idea may be to assign a separate handler to each button. But there’s a
more elegant solution. We can add a handler for the whole menu and data-
action attributes for buttons that has the method to call:
The handler reads the attribute and executes the method. Take a look at the
working example:
<div id="menu">
<button data-action="save">Save</button>
<button data-action="load">Load</button>
<button data-action="search">Search</button>
</div>
<script>
class Menu {
constructor(elem) {
this._elem = elem;
elem.onclick = this.onClick.bind(this); // (*)
}
save() {
alert('saving');
}
load() {
alert('loading');
}
search() {
alert('searching');
}
onClick(event) {
let action = event.target.dataset.action;
if (action) {
this[action]();
}
};
}
new Menu(menu);
</script>
Counter
For instance, here the attribute data-counter adds a behavior: “increase value
on click” to buttons:
<script>
document.addEventListener('click', function(event) {
});
</script>
If we click a button – its value is increased. Not buttons, but the general approach
is important here.
There can be as many attributes with data-counter as we want. We can add
new ones to HTML at any moment. Using the event delegation we “extended”
HTML, added an attribute that describes a new behavior.
⚠ For document-level handlers – always addEventListener
When we assign an event handler to the document object, we should
always use addEventListener , not document.onclick , because the
latter will cause conflicts: new handlers overwrite old ones.
For real projects it’s normal that there are many handlers on document set
by different parts of the code.
Toggler
One more example. A click on an element with the attribute data-toggle-id
will show/hide the element with the given id :
<button data-toggle-id="subscribe-mail">
Show the subscription form
</button>
<script>
document.addEventListener('click', function(event) {
let id = event.target.dataset.toggleId;
if (!id) return;
elem.hidden = !elem.hidden;
});
</script>
Let’s note once again what we did. Now, to add toggling functionality to an
element – there’s no need to know JavaScript, just use the attribute data-
toggle-id .
That may become really convenient – no need to write JavaScript for every such
element. Just use the behavior. The document-level handler makes it work for any
element of the page.
We can combine multiple behaviors on a single element as well.
The “behavior” pattern can be an alternative of mini-fragments of JavaScript.
Summary
Event delegation is really cool! It’s one of the most helpful patterns for DOM
events.
It’s often used to add same handling for many similar elements, but not only for
that.
The algorithm:
1. Put a single handler on the container.
2. In the handler – check the source element event.target .
3. If the event happened inside an element that interests us, then handle the
event.
Benefits:
●
First, the event must be bubbling. Some events do not bubble. Also,
low-level handlers should not use event.stopPropagation() .
● Second, the delegation may add CPU load, because the container-level
handler reacts on events in any place of the container, no matter if they
interest us or not. But usually the load is negligible, so we don’t take it
into account.
✔ Tasks
There’s a list of messages with removal buttons [x] . Make the buttons work.
Like this:
Horse [x]
The horse is one of two extant subspecies of Equus ferus. It is an
odd-toed ungulate mammal belonging to the taxonomic family
Equidae. The horse has evolved over the past 45 to 55 million years
from a small multi-toed creature, Eohippus, into the large, single-
toed animal of today.
Donkey [x]
The donkey or ass (Equus africanus asinus) is a domesticated
member of the horse family, Equidae. The wild ancestor of the
donkey is the African wild ass, E. africanus. The donkey has been
used as a working animal for at least 5000 years.
[x]
Cat
The domestic cat (Latin: Felis catus) is a small, typically furry,
carnivorous mammal. They are often called house cats when kept as
indoor pets or simply cats when there is no need to distinguish them
from other felids and felines. Cats are often valued by humans for
companionship and for their ability to hunt vermin.
P.S. Should be only one event listener on the container, use event delegation.
To solution
Tree menu
importance: 5
Requirements:
To solution
Sortable table
importance: 4
Make the table sortable: clicks on <th> elements should sort it by corresponding
column.
<table id="grid">
<thead>
<tr>
<th data-type="number">Age</th>
<th data-type="string">Name</th>
</tr>
</thead>
<tbody>
<tr>
<td>5</td>
<td>John</td>
</tr>
<tr>
<td>10</td>
<td>Ann</td>
</tr>
...
</tbody>
</table>
In the example above the first column has numbers, and the second one – strings.
The sorting function should handle sort according to the type.
Age Name
5 John
2 Pete
12 Ann
9 Eugene
1 Ilya
P.S. The table can be big, with any number of rows and columns.
To solution
Tooltip behavior
importance: 5
When a mouse comes over an element with data-tooltip , the tooltip should
appear over it, and when it’s gone then hide.
Scroll the page to make buttons appear on the top, check if the tooltips show up correctly.
In this task we assume that all elements with data-tooltip have only text
inside. No nested tags (yet).
Details:
● The tooltip should not cross window edges. Normally it should be above the
element, but if the element is at the page top and there’s no space for the
tooltip, then below it.
● The tooltip is given in the data-tooltip attribute. It can be arbitrary HTML.
Please use event delegation: set up two handlers on document to track all
“overs” and “outs” from elements with data-tooltip and manage tooltips from
there.
After the behavior is implemented, even people unfamiliar with JavaScript can add
annotated elements.
To solution
There are two ways to tell the browser we don’t want it to act:
● The main way is to use the event object. There’s a method
event.preventDefault() .
●
If the handler is assigned using on<event> (not by addEventListener ),
then we can just return false from it.
In all other cases, the return is not needed and it’s not processed anyhow.
Menu items are links <a> , not buttons. There are several benefits, for instance:
●
Many people like to use “right click” – “open in a new window”. If we use
<button> or <span> , that doesn’t work.
● Search engines follow <a href="..."> links while indexing.
menu.onclick = function(event) {
if (event.target.nodeName != 'A') return;
If we omit return false , then after our code executes the browser will do its
“default action” – following to the URL in href .
By the way, using event delegation here makes our menu flexible. We can add
nested lists and style them using CSS to “slide down”.
Certain events flow one into another. If we prevent the first event, there will be no
second.
For instance, mousedown on an <input> field leads to focusing in it, and the
focus event. If we prevent the mousedown event, there’s no focus.
Try to click on the first <input> below – the focus event happens. That’s
normal.
But if you click the second one, there’s no focus.
<input value="Focus works" onfocus="this.value=''">
<input onmousedown="return false" onfocus="this.value=''" value="Click me">
There are some events like touchmove on mobile devices (when the user
moves their finger across the screen), that cause scrolling by default, but that
scrolling can be prevented using preventDefault() in the handler.
So when the browser detects such event, it has first to process all handlers, and
then if preventDefault is not called anywhere, it can proceed with scrolling.
That may cause unnecessary delays and “jitters” in the UI.
The passive: true options tells the browser that the handler is not going to
cancel scrolling. Then browser scrolls immediately providing a maximally fluent
experience, and the event is handled by the way.
For some browsers (Firefox, Chrome), passive is true by default for
touchstart and touchmove events.
event.defaultPrevented
Let’s see a practical example where stopping the bubbling looks necessary, but
actually we can do well without it.
By default the browser on contextmenu event (right mouse click) shows a
context menu with standard options. We can prevent it and show our own, like
this:
Right-click for browser context menu Right-click for our context menu
Now let’s say we want to implement our own document-wide context menu, with
our options. And inside the document we may have other elements with their own
context menus:
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
alert("Button context menu");
};
document.oncontextmenu = function(event) {
event.preventDefault();
alert("Document context menu");
};
</script>
The problem is that when we click on elem , we get two menus: the button-level
and (the event bubbles up) the document-level menu.
How to fix it? One of solutions is to think like: “We fully handle the event in the
button handler, let’s stop it” and use event.stopPropagation() :
document.oncontextmenu = function(event) {
event.preventDefault();
alert("Document context menu");
};
</script>
Now the button-level menu works as intended. But the price is high. We forever
deny access to information about right-clicks for any outer code, including
counters that gather statistics and so on. That’s quite unwise.
An alternative solution would be to check in the document handler if the default
action was prevented? If it is so, then the event was handled, and we don’t need
to react on it.
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
alert("Button context menu");
};
document.oncontextmenu = function(event) {
if (event.defaultPrevented) return;
event.preventDefault();
alert("Document context menu");
};
</script>
Summary
All the default actions can be prevented if we want to handle the event exclusively
by JavaScript.
To prevent a default action – use either event.preventDefault() or
return false . The second method works only for handlers assigned with
on<event> .
The passive: true option of addEventListener tells the browser that the
action is not going to be prevented. That’s useful for some mobile events, like
touchstart and touchmove , to tell the browser that it should not wait for all
handlers to finish before scrolling.
If the default action was prevented, the value of event.defaultPrevented
becomes true , otherwise it’s false .
Besides being “just a good thing”, that makes your HTML better in terms of
accessibility.
Also if we consider the example with <a> , then please note: a browser allows
to open such links in a new window (by right-clicking them and other means).
And people like that. But if we make a button behave as a link using
JavaScript and even look like a link using CSS, then <a> -specific browser
features still won’t work for it.
✔ Tasks
<script>
function handler() {
alert( "..." );
return false;
}
</script>
<a href="http://w3.org" onclick="handler()">the browser will go to w3.org</a>
The browser follows the URL on click, but we don’t want it.
How to fix?
To solution
Make all links inside the element with id="contents" ask the user if they
really want to leave. And if they don’t then don’t follow.
Like this:
#contents
How about to read Wikipedia or visit W3.org and learn about modern standards?
Details:
To solution
Image gallery
importance: 5
Create an image gallery where the main image changes by the click on a
thumbnail.
Like this:
P.S. Use event delegation.
To solution
Also we can generate built-in events like click , mousedown etc, that may be
good for testing.
Event constructor
Events form a hierarchy, just like DOM element classes. The root is the built-in
Event class.
Arguments:
● event type – may be any string, like "click" or our own like "hey-ho!" .
●
options – the object with two optional properties:
●
bubbles: true/false – if true , then the event bubbles.
● cancelable: true/false – if true , then the “default action” may be
prevented. Later we’ll see what it means for custom events.
By default both are false: {bubbles: false, cancelable: false} .
dispatchEvent
After an event object is created, we should “run” it on an element using the call
elem.dispatchEvent(event) .
Then handlers react on it as if it were a regular built-in event. If the event was
created with the bubbles flag, then it bubbles.
In the example below the click event is initiated in JavaScript. The handler
works same way as if the button was clicked:
<script>
let event = new Event("click");
elem.dispatchEvent(event);
</script>
event.isTrusted
There is a way to tell a “real” user event from a script-generated one.
The property event.isTrusted is true for events that come from real
user actions and false for script-generated events.
Bubbling example
We can create a bubbling event with the name "hello" and catch it on
document .
<script>
// catch on document...
document.addEventListener("hello", function(event) { // (1)
alert("Hello from " + event.target.tagName); // Hello from H1
});
// ...dispatch on elem!
let event = new Event("hello", {bubbles: true}); // (2)
elem.dispatchEvent(event);
</script>
Notes:
1. We should use addEventListener for our custom events, because
on<event> only exists for built-in events, document.onhello doesn’t
work.
2. Must set bubbles:true , otherwise the event won’t bubble up.
The bubbling mechanics is the same for built-in ( click ) and custom ( hello )
events. There are also capturing and bubbling stages.
Here’s a short list of classes for UI Events from the UI Event specification :
●
UIEvent
● FocusEvent
●
MouseEvent
● WheelEvent
●
KeyboardEvent
● …
We should use them instead of new Event if we want to create such events.
For instance, new MouseEvent("click") .
The right constructor allows to specify standard properties for that type of event.
Like clientX/clientY for a mouse event:
alert(event.clientX); // 100
Please note: the generic Event constructor does not allow that.
Let’s try:
Custom events
For our own, custom events like "hello" we should use new CustomEvent .
Technically CustomEvent is the same as Event , with one exception.
In the second argument (object) we can add an additional property detail for
any custom information that we want to pass with the event.
For instance:
<script>
// additional details come with the event to the handler
elem.addEventListener("hello", function(event) {
alert(event.detail.name);
});
elem.dispatchEvent(new CustomEvent("hello", {
detail: { name: "John" }
}));
</script>
The detail property can have any data. Technically we could live without,
because we can assign any properties into a regular new Event object after its
creation. But CustomEvent provides the special detail field for it to evade
conflicts with other event properties.
The event class tells something about “what kind of event” it is, and if the event is
custom, then we should use CustomEvent just to be clear about what it is.
event.preventDefault()
Of course, if the event has a non-standard name, then it’s not known to the
browser, and there’s no “default browser action” for it.
But the event-generating code may plan some actions after dispatchEvent .
<pre id="rabbit">
|\ /|
\|_|/
/. .\
=\_Y_/=
{>o<}
</pre>
<script>
// hide() will be called automatically in 2 seconds
function hide() {
let event = new CustomEvent("hide", {
cancelable: true // without that flag preventDefault doesn't work
});
if (!rabbit.dispatchEvent(event)) {
alert('the action was prevented by a handler');
} else {
rabbit.hidden = true;
}
}
rabbit.addEventListener('hide', function(event) {
if (confirm("Call preventDefault?")) {
event.preventDefault();
}
});
// hide in 2 seconds
setTimeout(hide, 2000);
</script>
The exception is when one event is initiated from within another one.
Then the control jumps to the nested event handler, and after it goes back.
For instance, here the nested menu-open event is processed synchronously,
during the onclick :
<button id="menu">Menu (click me)</button>
<script>
// 1 -> nested -> 2
menu.onclick = function() {
alert(1);
// alert("nested")
menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
}));
alert(2);
};
Please note that the nested event menu-open bubbles up and is handled on the
document . The propagation of the nested event is fully finished before the
processing gets back to the outer code ( onclick ).
That’s not only about dispatchEvent , there are other cases. JavaScript in an
event handler can call methods that lead to other events – they are too processed
synchronously.
If we don’t like it, we can either put the dispatchEvent (or other event-
triggering call) at the end of onclick or wrap it in zero-delay setTimeout :
<script>
// Now the result is: 1 -> 2 -> nested
menu.onclick = function() {
alert(1);
// alert(2)
setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
})));
alert(2);
};
Summary
We shouldn’t generate browser events as it’s a hacky way to run handlers. That’s
a bad architecture most of the time.
Native events might be generated:
● As a dirty hack to make 3rd-party libraries work the needed way, if they don’t
provide other means of interaction.
● For automated testing, to “click the button” in the script and see if the interface
reacts correctly.
Custom events with our own names are often generated for architectural
purposes, to signal what happens inside our menus, sliders, carousels etc.
UI Events
Here we cover most important user interface events and how to work with them.
We can split mouse events into two categories: “simple” and “complex”
Simple events
The most used simple events are:
mousedown/mouseup
Mouse button is clicked/released over an element.
mouseover/mouseout
Mouse pointer comes over/out from an element.
mousemove
Every mouse move over an element triggers that event.
…There are several other event types too, we’ll cover them later.
Complex events
click
Triggers after mousedown and then mouseup over the same element if the left
mouse button was used.
contextmenu
Triggers after mousedown if the right mouse button was used.
dblclick
Triggers after a double click over an element.
Complex events are made of simple ones, so in theory we could live without them.
But they exist, and that’s good, because they are convenient.
Events order
An action may trigger multiple events.
For instance, a click first triggers mousedown , when the button is pressed, then
mouseup and click when it’s released.
In cases when a single action initiates multiple events, their order is fixed. That is,
the handlers are called in the order mousedown → mouseup → click .
Events are handled in the same sequence: onmouseup finishes before
onclick runs.
Click-related events always have the which property, which allows to get the
exact mouse button.
It is not used for click and contextmenu events, because the former
happens only on left-click, and the latter – only on right-click.
But if we track mousedown and mouseup , then we need it, because these
events trigger on any button, so which allows to distinguish between “right-
mousedown” and “left-mousedown”.
There are the three possible values:
● event.which == 1 – the left button
● event.which == 2 – the middle button
●
event.which == 3 – the right button
The middle button is somewhat exotic right now and is very rarely used.
All mouse events include the information about pressed modifier keys.
The properties are:
● shiftKey
●
altKey
● ctrlKey
●
metaKey ( Cmd for Mac)
<script>
button.onclick = function(event) {
if (event.altKey && event.shiftKey) {
alert('Hooray!');
}
};
</script>
Alt+Shift+Click on me!
On Windows and Linux there are modifier keys Alt , Shift and Ctrl . On
Mac there’s one more: Cmd , it corresponds to the property metaKey .
In most cases when Windows/Linux uses Ctrl , on Mac people use Cmd . So
where a Windows user presses Ctrl+Enter or Ctrl+A , a Mac user would
press Cmd+Enter or Cmd+A , and so on, most apps use Cmd instead of
Ctrl .
So if we want to support combinations like Ctrl +click, then for Mac it makes
sense to use Cmd +click. That’s more comfortable for Mac users.
Even if we’d like to force Mac users to Ctrl +click – that’s kind of difficult.
The problem is: a left-click with Ctrl is interpreted as a right-click on Mac,
and it generates the contextmenu event, not click like Windows/Linux.
For instance, if we have a window of the size 500x500, and the mouse is in the
left-upper corner, then clientX and clientY are 0 . And if the mouse is in
the center, then clientX and clientY are 250 , no matter what place in the
document it is. They are similar to position:fixed .
Document-relative coordinates are counted from the left-upper corner of the
document, not the window. Coordinates pageX , pageY are similar to
position:absolute on the document level.
No selection on mousedown
Mouse clicks have a side-effect that may be disturbing. A double click selects the
text.
If we want to handle click events ourselves, then the “extra” selection doesn’t look
good.
For instance, a double-click on the text below selects it in addition to our handler:
Double-click me
There’s a CSS way to stop the selection: the user-select property from CSS
UI Draft .
<style>
b {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>
Before...
<b ondblclick="alert('Test')">
Unselectable
</b>
...After
Before...
<b ondblclick="alert('Click!')" onmousedown="return false">
Double-click me
</b>
...After
Before...
<b ondblclick="getSelection().removeAllRanges()">
Double-click me
</b>
...After
If you double-click on the bold element, then the selection appears and then is
immediately removed. That doesn’t look nice though.
Preventing copying
If we want to disable selection to protect our content from copy-pasting, then
we can use another event: oncopy .
Dear user, The copying is forbidden for you. If you know JS or HTML, then you can get
everything from the page source though.
If you try to copy a piece of text in the <div> , that won’t work, because the
default action oncopy is prevented.
Surely that can’t stop the user from opening HTML-source, but not everyone
knows how to do it.
Summary
It’s also important to deal with text selection as an unwanted side-effect of clicks.
There are several ways to do this, for instance:
Selectable list
importance: 5
● A click on a list element selects only that element (adds the class
.selected ), deselects all others.
● If a click is made with Ctrl ( Cmd for Mac), then the selection is toggled on
the element, but other elements are not modified.
The demo:
Christopher Robin
Winnie-the-Pooh
Tigger
Kanga
Rabbit. Just rabbit.
P.S. For this task we can assume that list items are text-only. No nested tags.
P.P.S. Prevent the native browser selection of the text on clicks.
To solution
Mouseover/mouseout, relatedTarget
The mouseover event occurs when a mouse pointer comes over an element,
and mouseout – when it leaves.
These events are special, because they have a relatedTarget .
This property complements target . When a mouse leaves one element for
another, one of them becomes target , and the other one relatedTarget .
For mouseover :
● event.target – is the element where the mouse came over.
● event.relatedTarget – is the element from which the mouse came
( relatedTarget → target ).
That’s normal and just means that the mouse came not from another element,
but from out of the window. Or that it left the window.
Events frequency
The mousemove event triggers when the mouse moves. But that doesn’t mean
that every pixel leads to an event.
The browser checks the mouse position from time to time. And if it notices
changes then triggers the events.
That means that if the visitor is moving the mouse very fast then DOM-elements
may be skipped:
If the mouse moves very fast from #FROM to #TO elements as painted above,
then intermediate <div> (or some of them) may be skipped. The mouseout
event may trigger on #FROM and then immediately mouseover on #TO .
In particular it’s possible that the cursor jumps right inside the middle of the page
from out of the window. And relatedTarget=null , because it came from
“nowhere”:
The red <div> is nested inside the blue one. The blue <div> has
mouseover/out handlers that log all events in the textarea below.
Try entering the blue element and then moving the mouse on the red one – and
watch the events:
https://plnkr.co/edit/4PtAFMCaPdtyY0SbI3Zf?p=preview
So, for a handler that does not take target into account, it looks like we left the
parent in mouseout in (2) and returned back to it by mouseover in (3) .
If we perform some actions on entering/leaving the element, then we’ll get a lot of
extra “false” runs. For simple stuff that may be unnoticeable. For complex things
that may bring unwanted side-effects.
We can fix it by using mouseenter/mouseleave events instead.
When the pointer enters an element – the mouseenter triggers, and then
doesn’t matter where it goes while inside the element. The mouseleave event
only triggers when the cursor leaves it.
https://plnkr.co/edit/f5nodDg34O7ctGxOeb8Y?p=preview
Event delegation
Events mouseenter/leave are very simple and easy to use. But they do not
bubble. So we can’t use event delegation with them.
Imagine we want to handle mouse enter/leave for table cells. And there are
hundreds of cells.
The natural solution would be – to set the handler on <table> and process
events there. But mouseenter/leave don’t bubble. So if such event happens
on <td> , then only a handler on that <td> can catch it.
table.onmouseout = function(event) {
let target = event.target;
target.style.background = '';
};
These handlers work when going from any element to any inside the table.
But we’d like to handle only transitions in and out of <td> as a whole. And
highlight the cells as a whole. We don’t want to handle transitions that happen
between the children of <td> .
One of solutions:
● Remember the currently highlighted <td> in a variable.
● On mouseover – ignore the event if we’re still inside the current <td> .
●
On mouseout – ignore if we didn’t leave the current <td> .
That filters out “extra” events when we are moving between the children of <td> .
Summary
✔ Tasks
Write JavaScript that shows a tooltip over an element with the attribute data-
tooltip .
That’s like the task Tooltip behavior, but here the annotated elements can be
nested. The most deeply nested tooltip is shown.
For instance:
P.S. Hint: only one tooltip may show up at the same time.
To solution
"Smart" tooltip
importance: 5
Write a function that shows a tooltip over an element only if the visitor moves the
mouse over it, but not through it.
In other words, if the visitor moves the mouse on the element and stopped – show
the tooltip. And if they just moved the mouse through fast, then no need, who
wants extra blinking?
Technically, we can measure the mouse speed over the element, and if it’s slow
then we assume that it comes “over the element” and show the tooltip, if it’s fast –
then we ignore it.
The demo:
12 : 30 : 00
passes: 5 failures: 0 duration: 0.01s
hoverIntent
✓ mouseover -> immediately no tooltip ‣
✓ mouseover -> pause shows tooltip ‣
✓ mouseover -> fast mouseout no tooltip ‣
If you move the mouse over the “clock” fast then nothing happens, and if you do it
slow or stop on them, then there will be a tooltip.
Please note: the tooltip doesn’t “blink” when the cursor moves between the clock
subelements.
To solution
So here we’ll see how to implement Drag’n’Drop using mouse events. Not that
hard either.
Drag’n’Drop algorithm
These are the basics. We can extend it, for instance, by highlighting droppable
(available for the drop) elements when hovering over them.
moveAt(event.pageX, event.pageY);
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
}
};
If we run the code, we can notice something strange. On the beginning of the
drag’n’drop, the ball “forks”: we start dragging its “clone”.
That’s because the browser has its own Drag’n’Drop for images and some other
elements that runs automatically and conflicts with ours.
To disable it:
ball.ondragstart = function() {
return false;
};
But as we remember, mousemove triggers often, but not for every pixel. So after
swift move the cursor can jump from the ball somewhere in the middle of
document (or even outside of the window).
So we should listen on document to catch it.
Correct positioning
In the examples above the ball is always moved so, that it’s center is under the
pointer:
For instance, if we start dragging by the edge of the ball, then the cursor should
remain over the edge while dragging.
// onmousedown
let shiftX = event.clientX - ball.getBoundingClientRect().left;
let shiftY = event.clientY - ball.getBoundingClientRect().top;
2. Then while dragging we position the ball on the same shift relative to the
pointer, like this:
// onmousemove
// ball has position:absoute
ball.style.left = event.pageX - shiftX + 'px';
ball.style.top = event.pageY - shiftY + 'px';
ball.onmousedown = function(event) {
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
document.body.append(ball);
moveAt(event.pageX, event.pageY);
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
}
};
ball.ondragstart = function() {
return false;
};
In previous examples the ball could be dropped just “anywhere” to stay. In real-life
we usually take one element and drop it onto another. For instance, a file into a
folder, or a user into a trash can or whatever.
In other words, we take a “draggable” element and drop it onto “droppable”
element.
We need to know where the element was dropped at the end of Drag’n’Drop – to
do the corresponding action, and, preferably, know the droppable we’re dragging
over, to highlight it.
The solution is kind-of interesting and just a little bit tricky, so let’s cover it here.
What may be the first idea? Probably to set mouseover/mouseup handlers on
potential droppables and detect when the mouse pointer appears over them. And
then we know that we are dragging over/dropping on that element.
For instance, below are two <div> elements, red on top of blue. There’s no way
to catch an event on the blue one, because the red is on top:
<style>
div {
width: 50px;
height: 50px;
position: absolute;
top: 0;
}
</style>
<div style="background:blue" onmouseover="alert('never works')"></div>
<div style="background:red" onmouseover="alert('over red!')"></div>
The same with a draggable element. The ball is always on top over other
elements, so events happen on it. Whatever handlers we set on lower elements,
they won’t work.
That’s why the initial idea to put handlers on potential droppables doesn’t work in
practice. They won’t run.
So, what to do?
So in any of our mouse event handlers we can detect the potential droppable
under the pointer like this:
Please note: we need to hide the ball before the call (*) . Otherwise we’ll usually
have a ball on these coordinates, as it’s the top element under the pointer:
elemBelow=ball .
We can use that code to check what element we’re “flying over” at any time. And
handle the drop when it happens.
let currentDroppable = null; // potential droppable that we're flying over right
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
ball.hidden = true;
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
ball.hidden = false;
// mousemove events may trigger out of the window (when the ball is dragged off
// if clientX/clientY are out of the window, then elementfromPoint returns null
if (!elemBelow) return;
// potential droppables are labeled with the class "droppable" (can be other lo
let droppableBelow = elemBelow.closest('.droppable');
if (currentDroppable) {
// the logic to process "flying out" of the droppable (remove highlight)
leaveDroppable(currentDroppable);
}
currentDroppable = droppableBelow;
if (currentDroppable) {
// the logic to process "flying in" of the droppable
enterDroppable(currentDroppable);
}
}
}
In the example below when the ball is dragged over the soccer gate, the gate is
highlighted.
https://plnkr.co/edit/VdbuAVBTO0X3sIR8o66m?p=preview
Now we have the current “drop target”, that we’re flying over, in the variable
currentDroppable during the whole process and can use it to highlight or any
other stuff.
Summary
There are frameworks that build architecture over it: DragZone , Droppable ,
Draggable and other classes. Most of them do the similar stuff to described
above, so it should be easy to understand them now. Or roll our own, because you
already know how to handle the process, and it may be more flexible than to
adapt something else.
✔ Tasks
Slider
importance: 5
Create a slider:
Drag the blue thumb with the mouse and move it.
Important details:
● When the mouse button is pressed, during the dragging the mouse may go
over or below the slider. The slider will still work (convenient for the user).
● If the mouse moves very fast to the left or to the right, the thumb should stop
exactly at the edge.
To solution
This task can help you to check understanding of several aspects of Drag’n’Drop
and DOM.
Make all elements with class draggable – draggable. Like a ball in the chapter.
Requirements:
● Use event delegation to track drag start: a single event handler on document
for mousedown .
● If elements are dragged to top/bottom window edges – the page scrolls
up/down to allow further dragging.
● There is no horizontal scroll.
● Draggable elements should never leave the window, even after swift mouse
moves.
To solution
Keyboard: keydown and keyup
Before we get to keyboard, please note that on modern devices there are other
ways to “input something”. For instance, people use speech recognition
(especially on mobile devices) or copy/paste with the mouse.
So if we want to track any input into an <input> field, then keyboard events are
not enough. There’s another event named input to handle changes of an
<input> field, by any means. And it may be a better choice for such task. We’ll
cover it later in the chapter Events: change, input, cut, copy, paste.
Keyboard events should be used when we want to handle keyboard actions
(virtual keyboard also counts). For instance, to react on arrow keys Up and
Down or hotkeys (including combinations of keys).
Teststand
The keydown events happens when a key is pressed down, and then keyup –
when it’s released.
Z z (lowercase) KeyZ
If a user works with different languages, then switching to another language would
make a totally different character instead of "Z" . That will become the value of
event.key , while event.code is always the same: "KeyZ" .
“KeyZ” and other key codes
Every key has the code that depends on its location on the keyboard. Key
codes described in the UI Events code specification .
For instance:
● Letter keys have codes "Key<letter>" : "KeyA" , "KeyB" etc.
● Digit keys have codes: "Digit<number>" : "Digit0" , "Digit1" etc.
● Special keys are coded by their names: "Enter" , "Backspace" ,
"Tab" etc.
There are several widespread keyboard layouts, and the specification gives
key codes for each of them.
See alphanumeric section of the spec for more codes, or just try the
teststand above.
Please evade mistypes: it’s KeyZ , not keyZ . The check like
event.code=="keyZ" won’t work: the first letter of "Key" must be
uppercase.
What if a key does not give any character? For instance, Shift or F1 or others.
For those keys event.key is approximately the same as event.code :
F1 F1 F1
Please note that event.code specifies exactly which key is pressed. For
instance, most keyboards have two Shift keys: on the left and on the right side.
The event.code tells us exactly which one was pressed, and event.key is
responsible for the “meaning” of the key: what it is (a “Shift”).
Let’s say, we want to handle a hotkey: Ctrl+Z (or Cmd+Z for Mac). Most text
editors hook the “Undo” action on it. We can set a listener on keydown and
check which key is pressed – to detect when we have the hotkey.
There’s a dilemma here: in such a listener, should we check the value of
event.key or event.code ?
document.addEventListener('keydown', function(event) {
if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) {
alert('Undo!')
}
});
On the other hand, there’s a problem with event.code . For different keyboard
layouts, the same key may have different labels (letters).
For example, here are US layout (“QWERTY”) and German layout (“QWERTZ”)
under it (courtesy of Wikipedia):
For the same key, US layout has “Z”, while German layout has “Y” (letters are
swapped).
So, event.code will equal KeyZ for people with German layout when they
press “Y”.
That sounds odd, but so it is. The specification explicitly mentions such
behavior.
● event.code has the benefit of staying always the same, bound to the
physical key location, even if the visitor changes languages. So hotkeys that
rely on it work well even in case of a language switch.
●
event.code may match a wrong character for unexpected layout. Same
letters in different layouts may map to different physical keys, leading to
different codes. Luckily, that happens only with several codes, e.g. keyA ,
keyQ , keyZ (as we’ve seen), and doesn’t happen with special keys such as
Shift . You can find the list in the specification .
Auto-repeat
If a key is being pressed for a long enough time, it starts to “auto-repeat”: the
keydown triggers again and again, and then when it’s released we finally get
keyup . So it’s kind of normal to have many keydown and a single keyup .
Default actions
Default actions vary, as there are many possible things that may be initiated by
the keyboard.
For instance:
● A character appears on the screen (the most obvious outcome).
● A character is deleted ( Delete key).
●
The page is scrolled ( PageDown key).
● The browser opens the “Save Page” dialog ( Ctrl+S )
●
…and so on.
Preventing the default action on keydown can cancel most of them, with the
exception of OS-based special keys. For instance, on Windows Alt+F4 closes
the current browser window. And there’s no way to stop it by preventing the
default action in JavaScript.
For instance, the <input> below expects a phone number, so it does not accept
keys except digits, + , () or - :
<script>
function checkPhoneKey(key) {
return (key >= '0' && key <= '9') || key == '+' || key == '(' || key == ')' ||
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" ty
Phone, please
Please note that special keys like Backspace , Left , Right , Ctrl+V do not
work in the input. That’s a side-effect of the strict filter checkPhoneKey .
<script>
function checkPhoneKey(key) {
return (key >= '0' && key <= '9') || key == '+' || key == '(' || key == ')' ||
key == 'ArrowLeft' || key == 'ArrowRight' || key == 'Delete' || key == 'Backs
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" ty
Phone, please
Legacy
In the past, there was a keypress event, and also keyCode , charCode ,
which properties of the event object.
There were so many browser incompatibilities while working with them, that
developers of the specification had no way, other than deprecating all of them and
creating new, modern events (described above in this chapter). The old code still
works, as browsers keep supporting them, but there’s totally no need to use those
any more.
There was a time when this chapter included their detailed description. But, as of
now, browsers support modern events, so it was removed and replaced with more
details about the modern event handling.
Summary
Keyboard events:
● keydown – on pressing the key (auto-repeats if the key is pressed for long),
●
keyup – on releasing the key.
In the past, keyboard events were sometimes used to track user input in form
fields. That’s not reliable, because the input can come from various sources. We
have input and change events to handle any input (covered later in the
chapter Events: change, input, cut, copy, paste). They trigger after any kind of
input, including copy-pasting or speech recognition.
We should use keyboard events when we really want keyboard. For example, to
react on hotkeys or special keys.
✔ Tasks
Extended hotkeys
importance: 5
For instance, the code below shows alert when "Q" and "W" are pressed
together (in any language, with or without CapsLock)
runOnKeys(
() => alert("Hello!"),
"KeyQ",
"KeyW"
);
To solution
Scrolling
Scroll events allow to react on a page or element scrolling. There are quite a few
good things we can do here.
For instance:
● Show/hide additional controls or information depending on where in the
document the user is.
●
Load more data when the user scrolls down till the end of the page.
window.addEventListener('scroll', function() {
document.getElementById('showScroll').innerHTML = pageYOffset + 'px';
});
The scroll event works both on the window and on scrollable elements.
Prevent scrolling
For instance:
●
wheel event – a mouse wheel roll (a “scrolling” touchpad action generates it
too).
● keydown event for pageUp and pageDown .
Sometimes that may help, but it’s more reliable to use CSS to make something
unscrollable, such as the overflow property.
Here are few tasks that you can solve or look through to see the applications on
onscroll .
✔ Tasks
Endless page
importance: 5
Like this:
Scroll me
Date: Wed Jul 10 2019 19:13:04 GMT+0300 (Moscow Standard Time)
1. The scroll is “elastic”. We can scroll a little beyond the document start or
end in some browsers/devices (empty space below is shown, and then the
document will automatically “bounces back” to normal).
2. The scroll is imprecise. When we scroll to page end, then we may be in
fact like 0-50px away from the real document bottom.
So, “scrolling to the end” should mean that the visitor is no more than 100px away
from the document end.
P.S. In real life we may want to show “more messages” or “more goods”.
Open a sandbox for the task.
To solution
Up/down button
importance: 5
● While the page is not scrolled down at least for the window height – it’s
invisible.
● When the page is scrolled down more than the window height – there appears
an “upwards” arrow in the left-top corner. If the page is scrolled back, it
disappears.
● When the arrow is clicked, the page scrolls to the top.
Like this:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
103 104 105 106 107 108 109 110 111 112 113 114 115 116
117 118 119 120 121 122 123 124 125 126 127 128 129 130
131 132 133 134 135 136 137 138 139 140 141 142 143 144
145 146 147 148 149 150 151 152 153 154 155 156 157 158
159 160 161 162 163 164 165 166 167 168 169 170 171 172
173 174 175 176 177 178 179 180 181 182 183 184 185 186
To solution
Let’s say we have a slow-speed client and want to save their mobile traffic.
For that purpose we decide not to show images immediately, but rather replace
them with placeholders, like this:
Solar system
The Solar System is the gravitationally bound system comprising the Sun and the objects that orbit it,
either directly or indirectly. Of those objects that orbit the Sun directly, the largest eight are the
planets, with the remainder being significantly smaller objects, such as dwarf planets and small Solar
System bodies. Of the objects that orbit the Sun indirectly, the moons, two are larger than the smallest
planet, Mercury.
The Solar System formed 4.6 billion years ago from the gravitational collapse of a giant interstellar
molecular cloud. The vast majority of the system's mass is in the Sun, with most of the remaining
d h f ll l h d
Scroll it to see images load “on-demand”.
Requirements:
● When the page loads, those images that are on-screen should load
immediately, prior to any scrolling.
● Some images may be regular, without data-src . The code should not touch
them.
● Once an image is loaded, it should not reload any more when scrolled in/out.
P.S. If you can, make a more advanced solution that would “preload” images that
are one page below/after the current position.
To solution
Forms, controls
Special properties and events for forms <form> and controls: <input> ,
<select> and other.
Working with forms will be much more convenient when we learn them.
That’s a so-called “named collection”: it’s both named and ordered. We can use
both the name or the number in the document to get the form.
When we have a form, then any element is available in the named collection
form.elements .
For instance:
<form name="my">
<input name="one" value="1">
<input name="two" value="2">
</form>
<script>
// get the form
let form = document.forms.my; // <form name="my"> element
alert(elem.value); // 1
</script>
There may be multiple elements with the same name, that’s often the case with
radio buttons.
<script>
let form = document.forms[0];
These navigation properties do not depend on the tag structure. All elements, no
matter how deep they are in the form, are available in form.elements .
Fieldsets as “subforms”
A form may have one or many <fieldset> elements inside it. They also
support the elements property.
For instance:
<body>
<form id="form">
<fieldset name="userFields">
<legend>info</legend>
<input name="login" type="text">
</fieldset>
</form>
<script>
alert(form.elements.login); // <input name="login">
// we can get the input both from the form and from the fieldset
alert(fieldset.elements.login == form.elements.login); // true
</script>
</body>
⚠ Shorter notation: form.name
There’s a shorter notation: we can access the element as
form[index/name] .
That also works, but there’s a minor issue: if we access an element, and then
change its name , then it is still available under the old name (as well as under
the new one).
That’s easy to see in an example:
<form id="form">
<input name="login">
</form>
<script>
alert(form.elements.login == form.login); // true, the same <input>
// the direct access now can use both names: the new one and the old one
alert(form.username == form.login); // true
</script>
Backreference: element.form
<form id="form">
<input type="text" name="login">
</form>
<script>
// form -> element
let login = form.login;
Form elements
Let’s talk about form controls, pay attention to their specific features.
Like this:
The first way is the most obvious, but (2) and (3) are usually more
convenient.
Here is an example:
<select id="select">
<option value="apple">Apple</option>
<option value="pear">Pear</option>
<option value="banana">Banana</option>
</select>
<script>
// all three lines do the same thing
select.options[2].selected = true;
select.selectedIndex = 2;
select.value = 'banana';
</script>
Unlike most other controls, <select multiple> allows multiple choice. In that
case we need to walk over select.options to get all selected values.
Like this:
<script>
// get all selected values from multi-select
let selected = Array.from(select.options)
.filter(option => option.selected)
.map(option => option.value);
alert(selected); // blues,rock
</script>
new Option
This is rarely used on its own. But there’s still an interesting thing.
In the specification of the option element there’s a nice short syntax to create
<option> elements:
Parameters:
●
text – the text inside the option,
● value – the option value,
● defaultSelected – if true , then selected HTML-attribute is created,
● selected – if true , then the option is selected.
For instance:
selected
Is the option selected.
index
The number of the option among the others in its <select> .
text
Text content of the option (seen by the visitor).
Summary
Form navigation:
document.forms
A form is available as document.forms[name/index] .
form.elements
Form elements are available as form.elements[name/index] , or can use
just form[name/index] . The elements property also works for
<fieldset> .
element.form
Elements reference their form in the form property.
These are the basics to start working with forms. We’ll meet many examples
further in the tutorial. In the next chapter we’ll cover focus and blur events
that may occur on any element, but are mostly handled on forms.
✔ Tasks
There’s a <select> :
<select id="genres">
<option value="rock">Rock</option>
<option value="blues" selected>Blues</option>
</select>
Focusing: focus/blur
An element receives a focus when the user either clicks on it or uses the Tab key
on the keyboard. There’s also an autofocus HTML attribute that puts the focus
into an element by default when a page loads and other means of getting a focus.
There are important peculiarities when working with focus events. We’ll do the
best to cover them further on.
Events focus/blur
The focus event is called on focusing, and blur – when the element loses the
focus.
<style>
.invalid { border-color: red; }
#error { color: red }
</style>
<div id="error"></div>
<script>
input.onblur = function() {
if (!input.value.includes('@')) { // not email
input.classList.add('invalid');
error.innerHTML = 'Please enter a correct email.'
}
};
input.onfocus = function() {
if (this.classList.contains('invalid')) {
// remove the "error" indication, because the user wants to re-enter somethin
this.classList.remove('invalid');
error.innerHTML = "";
}
};
</script>
Methods focus/blur
<style>
.error {
background: red;
}
</style>
<script>
input.onblur = function() {
if (!this.value.includes('@')) { // not email
// show the error
this.classList.add("error");
// ...and put the focus back
input.focus();
} else {
this.classList.remove("error");
}
};
</script>
If we enter something into the input and then try to use Tab or click away from
the <input> , then onblur returns the focus back.
The list varies between browsers, but one thing is always correct: focus/blur
support is guaranteed for elements that a visitor can interact with: <button> ,
<input> , <select> , <a> and so on.
From the other hand, elements that exist to format something like <div> ,
<span> , <table> – are unfocusable by default. The method elem.focus()
doesn’t work on them, and focus/blur events are never triggered.
The purpose of this attribute is to specify the order number of the element when
Tab is used to switch between them.
That is: if we have two elements, the first has tabindex="1" , and the second
has tabindex="2" , then pressing Tab while in the first element – moves us to
the second one.
There are two special values:
● tabindex="0" makes the element the last one.
● tabindex="-1" means that Tab should ignore that element.
For instance, here’s a list. Click the first item and press Tab :
Click the first item and press Tab. Keep track of the order. Please note that man
<ul>
<li tabindex="1">One</li>
<li tabindex="0">Zero</li>
<li tabindex="2">Two</li>
<li tabindex="-1">Minus one</li>
</ul>
<style>
li { cursor: pointer; }
:focus { outline: 1px dashed green; }
</style>
Click the first item and press Tab. Keep track of the order. Please note that many subsequent Tabs
can move the focus out of the iframe with the example.
One
Zero
Two
Minus one
The order is like this: 1 - 2 - 0 (zero is always the last). Normally, <li>
does not support focusing, but tabindex full enables it, along with events and
styling with :focus .
elem.tabIndex works too
Delegation: focusin/focusout
For instance, we can’t put onfocus on the <form> to highlight it, like this:
Name Surname
The example above doesn’t work, because when user focuses on an <input> ,
the focus event triggers on that input only. It doesn’t bubble up. So
form.onfocus never triggers.
First, there’s a funny historical feature: focus/blur do not bubble up, but
propagate down on the capturing phase.
<form id="form">
<input type="text" name="name" value="Name">
<input type="text" name="surname" value="Surname">
</form>
<script>
// put the handler on capturing phase (last argument true)
form.addEventListener("focus", () => form.classList.add('focused'), true);
form.addEventListener("blur", () => form.classList.remove('focused'), true);
</script>
Name Surname
Second, there are focusin and focusout events – exactly the same as
focus/blur , but they bubble.
<form id="form">
<input type="text" name="name" value="Name">
<input type="text" name="surname" value="Surname">
</form>
<script>
form.addEventListener("focusin", () => form.classList.add('focused'));
form.addEventListener("focusout", () => form.classList.remove('focused'));
</script>
Name Surname
Summary
✔ Tasks
Editable div
importance: 5
When the user presses Enter or it loses focus, the <textarea> turns back
into <div> , and its content becomes HTML in <div> .
To solution
Edit TD on click
importance: 5
● On click – the cell should became “editable” (textarea appears inside), we can
change HTML. There should be no resize, all geometry should remain the
same.
● Buttons OK and CANCEL appear below the cell to finish/cancel the editing.
● Only one cell may be editable at a moment. While a <td> is in “edit mode”,
clicks on other cells are ignored.
● The table may have many cells. Use event delegation.
The demo:
Click on a table cell to edit it. Press OK or CANCEL when you finish.
To solution
Keyboard-driven mouse
importance: 4
P.S. Don’t put event handlers anywhere except the #mouse element. P.P.S. Don’t
modify HTML/CSS, the approach should be generic and work with any element.
To solution
Event: change
The change event triggers when the element has finished changing.
For text inputs that means that the event occurs when it loses focus.
For instance, while we are typing in the text field below – there’s no event. But
when we move the focus somewhere else, for instance, click on a button – there
will be a change event:
Button
<select onchange="alert(this.value)">
<option value="">Select something</option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
Select something
Event: input
The input event triggers every time after a value is modified by the user.
Unlike keyboard events, it triggers on any value change, even those that does not
involve keyboard actions: pasting with a mouse or using speech recognition to
dictate the text.
For instance:
oninput:
If we want to handle every modification of an <input> then this event is the best
choice.
On the other hand, input event doesn’t trigger on keyboard input and other
actions that do not involve value change, e.g. pressing arrow keys ⇦ ⇨ while in
the input.
For instance, the code below prevents all such events and shows what we are
trying to cut/copy/paste:
Please note, that it’s possible to copy/paste not just text, but everything. For
instance, we can copy a file in the OS file manager, and paste it.
There’s a list of methods in the specification that can work with different data
types including files, read/write to the clipboard.
But please note that clipboard is a “global” OS-level thing. Most browsers allow
read/write access to the clipboard only in the scope of certain user actions for the
safety, e.g. in onclick event handlers.
Also it’s forbidden to generate “custom” clipboard events with dispatchEvent
in all browsers except Firefox.
Summary
A value was
change For text inputs triggers on focus loss.
changed.
✔ Tasks
Deposit calculator
importance: 5
Create an interface that allows to enter a sum of bank deposit and percentage,
then calculates how much it will be after given periods of time.
Deposit calculator.
Initial deposit 10000
How many months? 12 (one year)
Interest per year? 5
Was: Becomes:
10000 10500
To solution
Event: submit
Both actions lead to submit event on the form. The handler can check the data,
and if there are errors, show them and call event.preventDefault() , then
the form won’t be sent to the server.
In the form below:
Both actions show alert and the form is not sent anywhere due to return
false :
<form onsubmit="alert('submit!');return false">
First: Enter in the input field <input type="text" value="text"><br>
Second: Click "submit": <input type="submit" value="Submit">
</form>
Method: submit
Then the submit event is not generated. It is assumed that if the programmer
calls form.submit() , then the script already did all related processing.
Sometimes that’s used to manually create and send a form, like this:
form.submit();
✔ Tasks
Modal form
importance: 5
● A user should type something into a text field and press Enter or the OK
button, then callback(value) is called with the value they entered.
● Otherwise if the user presses Esc or CANCEL, then callback(null) is
called.
In both cases that ends the input process and removes the form.
Requirements:
Usage example:
P.S. The source document has HTML/CSS for the form with fixed positioning, but
it’s up to you to make it modal.
Open a sandbox for the task.
To solution
DOMContentLoaded
document.addEventListener("DOMContentLoaded", ready);
// not "document.onDOMContentLoaded = ..."
For instance:
<script>
function ready() {
alert('DOM is ready');
// image is not yet loaded (unless was cached), so the size is 0x0
alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
}
document.addEventListener("DOMContentLoaded", ready);
</script>
But it doesn’t wait for the image to load. So alert shows zero sizes.
At the first sight DOMContentLoaded event is very simple. The DOM tree is
ready – here’s the event. There are few peculiarities though.
<script>
document.addEventListener("DOMContentLoaded", () => {
alert("DOM ready!");
});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></
<script>
alert("Library loaded, inline script executed");
</script>
In the example above, we first see “Library loaded…”, and then “DOM ready!” (all
scripts are executed).
⚠ Scripts with async , defer or type="module" don’t block
DOMContentLoaded
Script attributes async and defer , that we’ll cover a bit later, don’t block
DOMContentLoaded. JavaScript modules behave like defer , they don’t
block it too.
But there’s a pitfall. If we have a script after the style, then that script must wait
until the stylesheet loads:
The reason is that the script may want to get coordinates and other style-
dependent properties of elements, like in the example above. Naturally, it has to
wait for styles to load.
As DOMContentLoaded waits for scripts, it now waits for styles before them as
well.
For instance, if the page has a form with login and password, and the browser
remembered the values, then on DOMContentLoaded it may try to autofill them
(if approved by the user).
So if DOMContentLoaded is postponed by long-loading scripts, then autofill
also awaits. You probably saw that on some sites (if you use browser autofill) –
the login/password fields don’t get autofilled immediately, but there’s a delay till
the page fully loads. That’s actually the delay until the DOMContentLoaded
event.
window.onload
The load event on the window object triggers when the whole page is loaded
including styles, images and other resources.
<script>
window.onload = function() {
alert('Page loaded');
window.onunload
When a visitor leaves the page, the unload event triggers on window . We can
do something there that doesn’t involve a delay, like closing related popup
windows.
Naturally, unload event is when the user leaves us, and we’d like to save the
data on our server.
It sends the data in background. The transition to another page is not delayed: the
browser leaves the page, but still performs sendBeacon .
window.addEventListener("unload", function() {
navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
};
●
The request is sent as POST.
● We can send not only a string, but also forms and other formats, as described
in the chapter Fetch, but usually it’s a stringified object.
● The data is limited by 64kb.
When the sendBeacon request is finished, the browser probably has already left
the document, so there’s no way to get server response (which is usually empty
for analytics).
There’s also a keepalive flag for doing such “after-page-left” requests in fetch
method for generic network requests. You can find more information in the chapter
Fetch API.
If we want to cancel the transition to another page, we can’t do it here. But we can
use another event – onbeforeunload .
window.onbeforeunload
If a visitor initiated navigation away from the page or tries to close the window, the
beforeunload handler asks for additional confirmation.
If we cancel the event, the browser may ask the visitor if they are sure.
You can try it by running this code and then reloading the page:
window.onbeforeunload = function() {
return false;
};
For historical reasons, returning a non-empty string also counts as canceling the
event. Some time ago browsers used show it as a message, but as the modern
specification says, they shouldn’t.
Here’s an example:
window.onbeforeunload = function() {
return "There are unsaved changes. Leave now?";
};
The behavior was changed, because some webmasters abused this event
handler by showing misleading and annoying messages. So right now old
browsers still may show it as a message, but aside of that – there’s no way to
customize the message shown to the user.
readyState
Like this:
if (document.readyState == 'loading') {
// loading yet, wait for the event
document.addEventListener('DOMContentLoaded', work);
} else {
// DOM is ready!
work();
}
There’s also readystatechange event that triggers when the state changes,
so we can print all these states like this:
// current state
console.log(document.readyState);
Here’s a document with <iframe> , <img> and handlers that log events:
<script>
log('initial readyState:' + document.readyState);
The numbers in square brackets denote the approximate time of when it happens.
Events labeled with the same digit happen approximately at the same time (± a
few ms).
● document.readyState becomes interactive right before
DOMContentLoaded . These two things actually mean the same.
● document.readyState becomes complete when all resources
( iframe and img ) are loaded. Here we can see that it happens in about the
same time as img.onload ( img is the last resource) and
window.onload . Switching to complete state means the same as
window.onload . The difference is that window.onload always works
after all other load handlers.
Summary
<script src="https://javascript.info/article/script-async-defer/long.js?speed=1">
There are some workarounds to that. For instance, we can put a script at the
bottom of the page. Then it can see elements above it, and it doesn’t block the
page content from showing:
<body>
...all content is above the script...
<script src="https://javascript.info/article/script-async-defer/long.js?speed=1
</body>
But this solution is far from perfect. For example, the browser notices the script
(and can start downloading it) only after it downloaded the full HTML document.
For long HTML documents, that may be a noticeable delay.
Such things are invisible for people using very fast connections, but many people
in the world still have slower internet speeds and use far-from-perfect mobile
internet.
Luckily, there are two <script> attributes that solve the problem for us: defer
and async .
defer
The defer attribute tells the browser that it should go on working with the page,
and load the script “in background”, then run the script when it loads.
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defe
</script>
Deferred scripts keep their relative order, just like regular scripts.
So, if we have a long script first, and then a smaller one, then the latter one waits.
So, if we have several async scripts, they may execute in any order. Whatever
loads first – runs first:
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
</script>
Async scripts are great when we integrate an independent third-party script into
the page: counters, ads and so on, as they don’t depend on our scripts, and our
scripts shouldn’t wait for them:
The script starts loading as soon as it’s appended to the document (*) .
We can change the load-first order into the document order (just like regular
scripts) by explicitly setting async property to false :
script.async = false;
document.body.append(script);
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
script.async = false;
document.body.append(script);
}
Summary
Both async and defer have one common thing: they don’t block page
rendering. So the user can read page content and get acquanted with the page
immediately.
But there are also essential differences between them:
Order DOMContentLoaded
Load-first order. Their Irrelevant. May load and execute while the document has not yet
async document order doesn’t been fully downloaded. That happens if scripts are small or cached,
matter – which loads first and the document is long enough.
Document order (as they go Execute after the document is loaded and parsed (they wait if
defer
in the document). needed), right before DOMContentLoaded .
So the user may read the page, but some graphical components are probably
not ready yet.
There should be “loading” indication in proper places, not-working buttons
disabled, to clearly show the user what’s ready and what’s not.
In practice, defer is used for scripts that need the whole DOM and/or their
relative execution order is important. And async is used for independent scripts,
like counters or ads. And their relative execution order does not matter.
Loading a script
Let’s say we need to load a third-party script and call a function that resides there.
We can load it dynamically, like this:
let script = document.createElement('script');
script.src = "my.js";
document.head.append(script);
…But how to run the function that is declared inside that script? We need to wait
until the script loads, and only then we can call it.
Please note:
For our own scripts we could use JavaScript modules here, but they are not
widely adopted by third-party libraries.
script.onload
The main helper is the load event. It triggers after the script was loaded and
executed.
For instance:
script.onload = function() {
// the script creates a helper function "_"
alert(_); // the function is available
};
…And what if the loading failed? For instance, there’s no such script (error 404) or
the server or the server is down (unavailable).
script.onerror
Errors that occur during the loading of the script can be tracked on error event.
script.onerror = function() {
alert("Error loading " + this.src); // Error loading https://example.com/404.js
};
Please note that we can’t get HTTP error details here. We don’t know was it error
404 or 500 or something else. Just that the loading failed.
⚠ Important:
Events onload / onerror track only the loading itself.
Errors during script processing and execution are out of the scope of these
events. To track script errors, one can use window.onerror global handler.
Other resources
The load and error events also work for other resources, basically for any
resource that has an external src .
For example:
img.onload = function() {
alert(`Image loaded, size ${img.width}x${img.height}`);
};
img.onerror = function() {
alert("Error occurred while loading image");
};
Crossorigin policy
There’s a rule: scripts from one site can’t access contents of the other site. So,
e.g. a script at https://facebook.com can’t read the user’s mailbox at
https://gmail.com .
Or, to be more precise, one origin (domain/port/protocol triplet) can’t access the
content from another one. So even if we have a subdomain, or just another port,
these are different origins, no access to each other.
This rule also affects resources from other domains.
If we’re using a script from another domain, and there’s an error in it, we can’t get
error details.
For example, let’s take a script error.js that consists of a single (bad) function
call:
// error.js
noSuchFunction();
<script>
window.onerror = function(message, url, line, col, errorObj) {
alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script src="/article/onload-onerror/crossorigin/error.js"></script>
<script>
window.onerror = function(message, url, line, col, errorObj) {
alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script src="https://cors.javascript.info/article/onload-onerror/crossorigin/erro
Details may vary depending on the browser, but the idea is same: any information
about the internals of a script, including error stack traces, is hidden. Exactly
because it’s from another domain.
Why do we need error details?
There are many services (and we can build our own) that listen to global errors
using window.onerror , save errors and provide an interface to access and
analyze them. That’s great, as we can see real errors, triggered by our users. But
if a script comes from another origin, then there’s no much information about
errors in it, as we’ve just seen.
Similar cross-origin policy (CORS) is enforced for other types of resources as well.
To allow cross-origin access, the <script> tag needs to have
crossorigin attribute, plus the remote server must provide special
headers.
Please note:
You can read more about cross-origin access in the chapter Fetch: Cross-
Origin Requests. It describes fetch method for network requests, but the
policy is exactly the same.
Such thing as “cookies” is out of our current scope, but you can read about
them in the chapter Cookies, document.cookie.
In our case, we didn’t have any crossorigin attribute. So the cross-origin access
was prohibited. Let’s add it.
We can choose between "anonymous" (no cookies sent, one server-side
header needed) and "use-credentials" (sends cookies too, two server-side
headers needed).
<script>
window.onerror = function(message, url, line, col, errorObj) {
alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script crossorigin="anonymous" src="https://cors.javascript.info/article/onload-
Summary
Images <img> , external styles, scripts and other resources provide load and
error events to track their loading:
● load triggers on a successful load,
● error triggers on a failed load.
The only exception is <iframe> : for historical reasons it always triggers load ,
for any load completion, even if the page is not found.
The readystatechange event also works for resources, but is rarely used,
because load/error events are simpler.
✔ Tasks
Normally, images are loaded when they are created. So when we add <img> to
the page, the user does not see the picture immediately. The browser needs to
load it first.
For instance, this will show an alert after the images are loaded:
function loaded() {
alert("Images loaded")
}
In case of an error, the function should still assume the picture “loaded”.
In other words, the callback is executed when all images are either loaded or
errored out.
The function is useful, for instance, when we plan to show a gallery with many
scrollable images, and want to be sure that all images are loaded.
In the source document you can find links to test images, and also the code to
check whether they are loaded or not. It should output 300 .
To solution
Miscellaneous
Mutation observer
MutationObserver is a built-in object that observes a DOM element and fires
a callback in case of changes.
We’ll first see syntax, and then explore a real-world use case.
Syntax
observer.observe(node, config);
config is an object with boolean options “what kind of changes to react on”:
● childList – changes in the direct children of node ,
● subtree – in all descendants of node ,
● attributes – attributes of node ,
●
attributeOldValue – record the old value of attribute (infers
attributes ),
● characterData – whether to observe node.data (text content),
● characterDataOldValue – record the old value of node.data (infers
characterData ),
● attributeFilter – an array of attribute names, to observe only selected
ones.
<script>
let observer = new MutationObserver(mutationRecords => {
console.log(mutationRecords); // console.log(the changes)
});
observer.observe(elem, {
// observe everything except attributes
childList: true,
subtree: true,
characterDataOldValue: true
});
</script>
mutationRecords = [{
type: "characterData",
oldValue: "me",
target: <text node>,
// other properties empty
}];
If we select and remove the <b>me</b> altogether, we’ll get multiple mutations:
mutationRecords = [{
type: "childList",
target: <div#elem>,
removedNodes: [<b>],
nextSibling: <text node>,
previousSibling: <text node>
// other properties empty
}, {
type: "characterData"
target: <text node>
// ...details depend on how the browser handles the change
// it may coalesce two adjacent text nodes "Edit " and ", please" into one node
// or it can just delete the extra space after "Edit".
// may be one mutation or a few
}];
Observer use case
Let’s say we’re making a website about programming. Naturally, articles and other
materials may contain source code snippets.
...
<pre class="language-javascript"><code>
// here's the code
let hello = "world";
</code></pre>
...
Also we’ll use a JavaScript highlighting library on our site, e.g. Prism.js . A call
to Prism.highlightElem(pre) examines the contents of such pre
elements and adds into them special tags and styles for colored syntax
highlighting, similar to what you see in examples here, at this page.
When to run that method? We can do it on DOMContentLoaded event, or at the
bottom of the page. At that moment we have DOM ready, can search for elements
pre[class*="language"] and call Prism.highlightElem on them:
Now the <pre> snippet looks like this (without line numbers by default):
Everything’s simple so far, right? There are <pre> code snippets in HTML, we
highlight them.
Now let’s go on. Let’s say we’re going to dynamically fetch materials from a
server. We’ll study methods for that later in the tutorial. For now it only matters
that we fetch an HTML article from a webserver and display it on demand:
The new article HTML may contain code snippets. We need to call
Prism.highlightElem on them, otherwise they won’t get highlighted.
We could append that call to the code that loads an article, like this:
…But imagine, we have many places in the code where we load contents: articles,
quizzes, forum posts. Do we need to put the highlighting call everywhere? That’s
not very convenient, and also easy to forget.
And what if the content is loaded by a third-party module? E.g. we have a forum
written by someone else, that loads contents dynamically, and we’d like to add
syntax highlighting to it. No one likes to patch third-party scripts.
});
The code below populates innerHTML . Please run the code above first, it will
watch and highlight the new content:
Additional methods
Additionally:
● mutationRecords = observer.takeRecords() – gets a list of
unprocessed mutation records, those that happened, but the callback did not
handle them.
Garbage collection
Observers use weak references to nodes internally. That is: if a node is removed
from DOM, and becomes unreachable, then it becomes garbage collected, an
observer doesn’t prevent that.
Summary
JavaScript can do everything with it: get the existing selection, select/deselect it or
its parts, remove the selected part from the document, wrap it into a tag, and so
on.
You can get a few ready to use recipes at the end, in “Summary” section. But
you’ll get much more if you read on. The underlying Range and Selection
objects are easy to grasp, and then you’ll need no recipes to make them do what
you want.
Range
Each point represented as a parent DOM node with the relative offset from its
start. For an element node, the offset is a child number, for a text node it’s the
position in the text.
First, we can create a range (the constructor has no parameters):
Here’s its DOM structure, note that here text nodes are important for us:
▾P
#text Example:
▾I
#text italic
#text and
▾B
#text bold
<script>
let range = new Range();
range.setStart(p, 0);
range.setEnd(p, 2);
Here’s a more flexible test stand where you try more variants:
range.setStart(p, start.value);
range.setEnd(p, end.value);
That’s also possible, we just need to set the start and the end as a relative offset
in text nodes.
We need to create a range, that:
● starts from position 2 in <p> first child (taking all but two first letters of
"Example: ")
● ends at the position 3 in <b> first child (taking first three letters of “bold”):
<script>
let range = new Range();
range.setStart(p.firstChild, 2);
range.setEnd(p.querySelector('b').firstChild, 3);
Range methods
As it was demonstrated, node can be both a text or element node: for text
nodes offset skips that many of characters, while for element nodes that
many child nodes.
Others:
● selectNode(node) set range to select the whole node
● selectNodeContents(node) set range to select the whole node
contents
● collapse(toStart) if toStart=true set end=start, otherwise set
start=end, thus collapsing the range
● cloneRange() creates a new range with the same start/end
<p id="result"></p>
<script>
let range = new Range();
range.setStart(p.firstChild, 2);
range.setEnd(p.querySelector('b').firstChild, 3);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
}
};
methods.resetExample();
</script>
deleteContents
extractContents
cloneContents
insertNode
surroundContents
resetExample
There also exist methods to compare ranges, but these are rarely used. When
you need them, please refer to the spec or MDN manual .
Selection
Range is a generic object for managing selection ranges. We may create such
objects, pass them around – they do not visually select anything on their own.
The document selection is represented by Selection object, that can be
obtained as window.getSelection() or document.getSelection() .
A selection may include zero or more ranges. At least, the Selection API
specification says so. In practice though, only Firefox allows to select multiple
ranges in the document by using Ctrl+click ( Cmd+click for Mac).
Selection properties
Similar to a range, a selection has a start, called “anchor”, and the end, called
“focus”.
The main selection properties are:
● anchorNode – the node where the selection starts,
● anchorOffset – the offset in anchorNode where the selection starts,
● focusNode – the node where the selection ends,
● focusOffset – the offset in focusNode where the selection ends,
● isCollapsed – true if selection selects nothing (empty range), or doesn’t
exist.
● rangeCount – count of ranges in the selection, maximum 1 in all browsers
except Firefox.
Selection end may be in the document before start
There are many ways to select the content, depending on the user agent:
mouse, hotkeys, taps on a mobile etc.
Some of them, such as a mouse, allow the same selection can be created in
two directions: “left-to-right” and “right-to-left”.
If the start (anchor) of the selection goes in the document before the end
(focus), this selection is said to have “forward” direction.
E.g. if the user starts selecting with mouse and goes from “Example” to “italic”:
That’s different from Range objects that are always directed forward: the
range start can’t be after its end.
Selection events
And here’s the demo of getting the selection both as text and as DOM nodes:
<script>
document.onselectionchange = function() {
let selection = document.getSelection();
// Get as text
astext.innerHTML += selection;
};
</script>
Selection methods
So, for many tasks we can call Selection methods, no need to access the
underlying Range object.
<script>
// select from 0th child of <p> to the last child
document.getSelection().setBaseAndExtent(p, 0, p, p.childNodes.length);
</script>
<script>
let range = new Range();
range.selectNodeContents(p); // or selectNode(p) to select the <p> tag too
Form elements, such as input and textarea provide API for selection in their
values .
As the value is a pure text, not HTML, these methods to not use Selection or
Range objects, they are much simpler.
● input.select() – selects everything in the text control,
●
input.selectionStart – position of selection start (writeable),
● input.selectionEnd – position of selection start (writeable),
●
input.selectionDirection – direction, one of: “forward”, “backward” or
“none” (if e.g. selected with a double mouse click),
● input.setSelectionRange(start, end, [direction]) – change
the selection to span from start till end , in the given direction (optional).
The last argument, selectionMode , determines how the selection will be set
after the text has been replaced. The possible values are:
● "select" – the newly inserted text will be selected.
●
"start" – the selection range collapses just before the inserted text.
● "end" – the selection range collapses just after the inserted text.
● "preserve" – attempts to preserve the selection. This is the default.
<script>
area.onselect = function() {
from.value = area.selectionStart;
to.value = area.selectionEnd;
};
</script>
<script>
area.onfocus = () => {
// zero delay setTimeout is needed
// to trigger after browser focus action
setTimeout(() => {
// we can set any selection
// if start=end, the cursor it exactly at that place
area.selectionStart = area.selectionEnd = 10;
});
};
</script>
…Or to insert something “at the cursor” using setRangeText .
Here’s an button that replaces the selection with "TEXT" and puts the cursor
immediately after it. If the selection is empty, the text is just inserted at the cursor
position:
<button id="button">Insert!</button>
<script>
button.onclick = () => {
// replace range with TEXT and collapse the selection at its end
area.setRangeText("TEXT", area.selectionStart, area.selectionEnd, "end");
};
</script>
</body>
Making unselectable
<style>
#elem {
user-select: none;
}
</style>
<div>Selectable <div id="elem">Unselectable</div> Selectable</div>
This doesn’t allow the selection to start at elem . But the user may start the
selection elsewhere and include elem into it.
<script>
elem.onselectstart = () => false;
</script>
This prevents starting the selection on elem , but the visitor may start it at
another element, then extend to elem .
That’s convenient when there’s another event handler on the same action that
triggers the select. So we disable the selection to avoid conflict.
References
● DOM spec: Range
● Selection API
Summary
// directly:
selection.setBaseAndExtent(...from...to...);
Another important thing to know about selection: the cursor position in editable
elements, like <textarea> is always at the start or the end of the selection.
We can use it both to get cursor position and to move the cursor by setting
elem.selectionStart and elem.selectionEnd .
In this chapter we first cover theoretical details about how things work, and then
see practical applications of that knowledge.
Event Loop
The concept of event loop is very simple. There’s an endless loop, when
JavaScript engine waits for tasks, executes them and then sleeps waiting for more
tasks.
1. While there are tasks:
● execute the oldest task.
2. Sleep until a task appears, then go to 1.
That’s a formalized algorithm for what we see when browsing a page. JavaScript
engine does nothing most of the time, only runs if a script/handler/event activates.
A task can be JS-code triggered by events, but can also be something else, e.g.:
● When an external script <script src="..."> loads, the task is to execute
it.
● When a user moves their mouse, the task is to dispatch mousemove event
and execute handlers.
● When the time is due for a scheduled setTimeout , the task is to run its
callback.
● …and so on.
Tasks are set – the engine handles them – then waits for more tasks (while
sleeping and consuming close to zero CPU).
It may happen that a task comes while the engine is busy, then it’s enqueued.
The tasks form a queue, so-called “macrotask queue” (v8 term):
For instance, while the engine is busy executing a script , a user may move
their mouse causing mousemove , and setTimeout may be due and so on,
these tasks form a queue, as illustrated on the picture above.
Tasks from the queue are processed on “first come – first served” basis. When the
engine browser finishes with fetch , it handles mousemove event, then
setTimeout handler, and so on.
Doesn’t matter if the task takes a long time. Changes to DOM are painted only
after the task is complete.
2. If a task takes too long, the browser can’t do other tasks, process user events,
so after a time it raises an alert like “Page Unresponsive” and suggesting to kill
the task with the whole page.
So we can split the long text into pieces. Highlight first 100 lines, then schedule
another 100 lines using zero-delay setTimeout , and so on.
If you run the code below, the engine will “hang” for some time. For server-side JS
that’s clearly noticeable, and if you are running it in-browser, then try to click other
buttons on the page – you’ll see that no other events get handled until the
counting finishes.
let i = 0;
function count() {
// do a heavy job
for (let j = 0; j < 1e9; j++) {
i++;
}
count();
The browser may even show “the script takes too long” warning (but hopefully it
won’t, because the number is not very big).
let i = 0;
function count() {
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
} else {
setTimeout(count); // schedule the new call (**)
}
count();
Now the browser interface is fully functional during the “counting” process.
A single run of count does a part of the job (*) , and then re-schedules itself
(**) if needed:
Now, if a new side task (e.g. onclick event) appears while the engine is busy
executing part 1, it gets queued and then executes when part 1 finished, before
the next part. Periodic returns to event loop between count executions provide
just enough “air” for the JavaScript engine to do something else, to react on other
user actions.
The notable thing is that both variants – with and without splitting the job by
setTimeout – are comparable in speed. There’s no much difference in the
overall counting time.
To make them closer, let’s make an improvement.
let i = 0;
function count() {
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
}
count();
Now when we start to count() and see that we’ll need to count() more, we
schedule that immediately, before doing the job.
If you run it, it’s easy to notice that it takes significantly less time.
Why?
That’s simple: as you remember, there’s the in-browser minimal delay of 4ms for
many nested setTimeout calls. Even if we set 0 , it’s 4ms (or a bit more). So
the earlier we schedule it – the faster it runs.
Another benefit of splitting heavy tasks for browser scripts is that we can show
progress indication.
Usually the browser renders after the currently running code is complete. Doesn’t
matter if the task takes a long time. Changes to DOM are painted only after the
task is finished.
From one hand, that’s great, because our function may create many elements,
add them one-by-one to the document and change their styles – the visitor won’t
see any “intermediate”, unfinished state. An important thing, right?
Here’s the demo, the changes to i won’t show up until the function finishes, so
we’ll see only the last value:
<div id="progress"></div>
<script>
function count() {
for (let i = 0; i < 1e6; i++) {
i++;
progress.innerHTML = i;
}
}
count();
</script>
…But we also may want to show something during the task, e.g. a progress bar.
If we split the heavy task into pieces using setTimeout , then changes are
painted out in-between them.
This looks prettier:
<div id="progress"></div>
<script>
let i = 0;
function count() {
if (i < 1e7) {
setTimeout(count);
}
count();
</script>
In an event handler we may decide to postpone some actions until the event
bubbled up and was handled on all levels. We can do that by wrapping the code in
zero delay setTimeout .
menu.onclick = function() {
// ...
Microtasks
After every macrotask, the engine executes all tasks from microtask queue, prior
to running any other macrotasks.
Promise.resolve()
.then(() => alert("promise"));
alert("code");
So if we’d like our code to execute asynchronously, but want the application state
be basically the same (no mouse coordinate changes, no new network data, etc),
then we can achieve that by creating a microtask with queueMicrotask .
<div id="progress"></div>
<script>
let i = 0;
function count() {
if (i < 1e6) {
queueMicrotask(count);
}
count();
</script>
So, microtasks are asynchronous from the point of code execution, but they don’t
allow any browser processes or events to stick in-between them.
Summary
1. Dequeue and run the oldest task from the macrotask queue (e.g. “script”).
2. Execute all microtasks:
● While the microtask queue is not empty:
● Dequeue and run the oldest microtask.
3. Render changes if any.
4. Wait until the macrotask queue is not empty (if needed).
5. Go to step 1.
That may be used to split a big calculation-heavy task into pieces, for the browser
to be able to react on user events and show progress between them.
Also, used in event handlers to schedule an action after the event is fully handled
(bubbling done).
Web Workers
For long heavy calculations that shouldn’t block the event loop, we can use
Web Workers .
Web Workers do not have access to DOM, so they are useful, mainly, for
calculations, to use multiplle CPU cores simultaneously.
Solutions
Walking the DOM
DOM children
document.body.firstElementChild
// or
document.body.children[0]
// or (the first node is space, so we take 2nd)
document.body.childNodes[1]
document.body.lastElementChild
// or
document.body.children[1]
Please note: for both cases if there are no children, then there will be an
error.
To formulation
To formulation
To formulation
Count descendants
We can read the text from the first child node of li , that is the text node:
for (let li of document.querySelectorAll('li')) {
let title = li.firstChild.data;
To formulation
<html>
<body>
<script>
alert(document.body.lastChild.nodeType);
</script>
</body>
</html>
To formulation
Tag in comment
<script>
let body = document.body;
To formulation
Or:
alert(document.constructor.name); // HTMLDocument
Yeah, we could browse the specification, but it would be faster to figure out
manually.
alert(HTMLDocument.prototype.constructor.name); // HTMLDocument
alert(HTMLDocument.prototype.__proto__.constructor.name); // Document
alert(HTMLDocument.prototype.__proto__.__proto__.constructor.name); // Nod
To formulation
<!DOCTYPE html>
<html>
<body>
<script>
// getting it
let elem = document.querySelector('[data-widget-name]');
To formulation
link.style.color = 'orange';
}
To formulation
Answer: 1 and 3.
Both commands result in adding the text “as text” into the elem .
Here’s an example:
<div id="elem1"></div>
<div id="elem2"></div>
<div id="elem3"></div>
<script>
let text = '<b>text</b>';
elem1.append(document.createTextNode(text));
elem2.innerHTML = text;
elem3.textContent = text;
</script>
To formulation
function clear(elem) {
for (let i=0; i < elem.childNodes.length; i++) {
elem.childNodes[i].remove();
}
}
That won’t work, because the call to remove() shifts the collection
elem.childNodes , so elements start from the index 0 every time. But
i increases, and some elements will be skipped.
function clear(elem) {
while (elem.firstChild) {
elem.firstChild.remove();
}
}
function clear(elem) {
elem.innerHTML = '';
}
To formulation
The HTML in the task is incorrect. That’s the reason of the odd thing.
The browser has to fix it automatically. But there may be no text inside the
<table> : according to the spec only table-specific tags are allowed. So
the browser adds "aaa" before the <table> .
The question can be easily answered by exploring the DOM using the
browser tools. It shows "aaa" before the <table> .
The HTML standard specifies in detail how to process bad HTML, and
such behavior of the browser is correct.
To formulation
Create a list
To formulation
To formulation
To append text to each <li> we can alter the text node data .
Open the solution in a sandbox.
To formulation
Create a calendar
The algorithm:
To formulation
Each component of the time would look great in its own <span> :
<div id="clock">
<span class="hour">hh</span>:<span class="min">mm</span>:<span class="se
</div>
function update() {
let clock = document.getElementById('clock');
let date = new Date(); // (*)
let hours = date.getHours();
if (hours < 10) hours = '0' + hours;
clock.children[0].innerHTML = hours;
In the line (*) we every time check the current date. The calls to
setInterval are not reliable: they may happen with delays.
let timerId;
function clockStop() {
clearInterval(timerId);
timerId = null;
}
To formulation
Insert the HTML in the list
The solution:
one.insertAdjacentHTML('afterend', '<li>2</li><li>3</li>');
To formulation
The solution is short, yet may look a bit tricky, so here I provide it with
extensive comments:
table.tBodies[0].append(...sortedRows);
1.
2.
3.
Then sort them comparing by the content of the first <td> (the name
field).
4.
Please note: we don’t have to remove them, just “re-insert”, they leave
the old place automatically.
To formulation
Create a notification
To formulation
In other words: (full height) minus (scrolled out top part) minus (visible
part) – that’s exactly the scrolled out bottom part.
To formulation
To get the scrollbar width, we can create an element with the scroll, but
without borders and paddings.
Then the difference between its full width offsetWidth and the inner
content area width clientWidth will be exactly the scrollbar:
div.style.overflowY = 'scroll';
div.style.width = '50px';
div.style.height = '50px';
div.remove();
alert(scrollWidth);
To formulation
The coordinates start from the inner left-upper corner of the field:
.........................
.........................
.........................
.........................
.........................
.........................
.........................
To align the ball center with the center of the field, we should move the ball
to the half of its width to the left and to the half of its height to the top:
When the browser does not know the width/height of an image (from tag
attributes or CSS), then it assumes them to equal 0 until the image
finishes loading.
After the first load browser usually caches the image, and on next loads it
will have the size immediately. But on the first load the value of
ball.offsetWidth is 0 . That leads to wrong coordinates.
#ball {
width: 40px;
height: 40px;
}
Open the solution in a sandbox.
To formulation
Differences:
To formulation
Coordinates
Outer corners
That differs from the outer corner by the border width. A reliable way to get
the distance is clientLeft/clientTop :
In our case we need to substract the border size from the outer
coordinates.
let answer4 = [
coords.right - parseInt(getComputedStyle(field).borderRightWidth),
coords.bottom - parseInt(getComputedStyle(field).borderBottomWidth)
];
let answer4 = [
coords.left + elem.clientLeft + elem.clientWidth,
coords.top + elem.clientTop + elem.clientHeight
];
To formulation
In this task we only need to accurately calculate the coordinates. See the
code for details.
To formulation
To formulation
Hide on click
To formulation
Hide self
Can use this in the handler to reference “the element itself” here:
To formulation
Which handlers run?
function handler() {
alert(1);
}
button.addEventListener("click", handler);
button.removeEventListener("click", handler);
To formulation
We can’t use position:fixed for it, because scrolling the page would
move the ball from the field.
#field {
width: 200px;
height: 150px;
position: relative;
}
#ball {
position: absolute;
left: 0; /* relative to the closest positioned ancestor (field) */
top: 0;
transition: 1s all; /* CSS animation for left/top makes the ball fly */
}
Please note that the ball width/height must be known at the time we
access ball.offsetWidth . Should be specified in HTML or CSS.
To formulation
HTML/CSS
<div class="menu">
<span class="title">Sweeties (click me)!</span>
<ul>
<li>Cake</li>
<li>Donut</li>
<li>Honey</li>
</ul>
</div>
Like this:
So if we set onclick on it, then it will catch clicks to the right of the text.
Toggling the menu should change the arrow and show/hide the menu list.
.menu ul {
margin: 0;
list-style: none;
padding-left: 20px;
display: none;
}
.menu .title::before {
content: '▶ ';
font-size: 80%;
color: green;
}
…And with .open the arrow changes and the list shows up:
.menu.open .title::before {
content: '▼ ';
}
.menu.open ul {
display: block;
}
To formulation
To formulation
Carousel
To do the scrolling, we can shift <ul> . There are many ways to do it, for
instance by changing margin-left or (better performance) use
transform: translateX() :
The outer <div> has a fixed width, so “extra” images are cut.
To formulation
Event delegation
To formulation
Tree menu
1. Wrap every tree node title into <span> . Then we can CSS-style
them on :hover and handle clicks exactly on text, because <span>
width is exactly the text width (unlike without it).
2. Set a handler to the tree root node and handle clicks on that
<span> titles.
To formulation
Sortable table
To formulation
Tooltip behavior
To formulation
When the browser reads the on* attribute like onclick , it creates the
handler from its content.
function(event) {
handler() // the content of onclick
}
Now we can see that the value returned by handler() is not used and
does not affect the result.
<script>
function handler() {
alert("...");
return false;
}
</script>
<script>
function handler(event) {
alert("...");
event.preventDefault();
}
</script>
To formulation
In real life instead of asking we can send a “logging” request to the server
that saves the information about where the visitor left. Or we can load the
content and show it right in the page (if allowable).
To formulation
Image gallery
The solution is to assign the handler to the container and track clicks. If a
click is on the <a> link, then change src of #largeImg to the href
of the thumbnail.
To formulation
To formulation
To formulation
"Smart" tooltip
The first idea would be: to run our function every 100ms and measure the
distance between previous and new coordinates. If it’s small, then the
speed is small.
P.S. Please note: the solution tests use dispatchEvent to see if the
tooltip works right.
To formulation
Slider
To formulation
To formulation
Keyboard: keydown and keyup
Extended hotkeys
The first handler adds to it, while the second one removes from it. Every
time on keydown we check if we have enough keys pressed, and run the
function if it is so.
To formulation
Scrolling
Endless page
The core of the solution is a function that adds more dates to the page (or
loads more stuff in real-life) while we’re at the page end.
The most important question is: “How do we detect that the page is
scrolled to bottom?”
For instance, if the height of the whole HTML document is 2000px, then:
// When we're on the top of the page
// window-relative top = 0
document.documentElement.getBoundingClientRect().top = 0
When we scroll till the end, assuming that the window height is 600px :
Please note that the bottom can’t be 0, because it never reaches the
window top. The lowest limit of the bottom coordinate is the window height,
we can’t scroll it any more up.
We want the document bottom be no more than 100px away from it.
function populate() {
while(true) {
// document bottom
let windowRelativeBottom = document.documentElement.getBoundingClient
// if it's greater than window height + 100px, then we're not at the
// (see examples above, big bottom means we need to scroll more)
if (windowRelativeBottom > document.documentElement.clientHeight + 100
To formulation
Up/down button
To formulation
The onscroll handler should check which images are visible and show
them.
We also may want to run it when the page loads, to detect immediately
visible images prior to any scrolling and load them.
If we put it at the <body> bottom, then it runs when the page content is
loaded.
function isVisible(elem) {
showVisible();
window.onscroll = showVisible;
To formulation
<select id="genres">
<option value="rock">Rock</option>
<option value="blues" selected>Blues</option>
</select>
<script>
// 1)
let selectedOption = genres.options[genres.selectedIndex];
alert( selectedOption.value );
// 2)
let newOption = new Option("Classic", "classic");
genres.append(newOption);
// 3)
newOption.selected = true;
</script>
To formulation
Focusing: focus/blur
Editable div
Edit TD on click
To formulation
Keyboard-driven mouse
We can use mouse.onclick to handle the click and make the mouse
“moveable” with position:fixed , then mouse.onkeydown to
handle arrow keys.
The only pitfall is that keydown only triggers on elements with focus. So
we need to add tabindex to the element. As we’re forbidden to change
HTML, we can use mouse.tabIndex property for that.
To formulation
Deposit calculator
To formulation
Forms: event and method submit
Modal form
#cover-div {
position: fixed;
top: 0;
left: 0;
z-index: 9000;
width: 100%;
height: 100%;
background-color: gray;
opacity: 0.3;
}
Because the <div> covers everything, it gets all clicks, not the page
below it.
The form should be not in the <div> , but next to it, because we don’t
want it to have opacity .
To formulation
The algorithm:
To formulation
Part 3
Additional articles
Ilya Kantor
Built at July 10, 2019
We constantly work to improve the tutorial. If you find any mistakes, please write at
our github.
● Frames and windows
● Popups and window methods
● Cross-window communication
● The clickjacking attack
●
Binary data, files
● ArrayBuffer, binary arrays
● TextDecoder and TextEncoder
● Blob
● File and FileReader
● Network requests
●
Fetch
● FormData
● Fetch: Download progress
● Fetch: Abort
● Fetch: Cross-Origin Requests
● Fetch API
● URL objects
● XMLHttpRequest
●
Resumable file upload
● Long polling
●
WebSocket
● Server Sent Events
●
Storing data in the browser
●
Cookies, document.cookie
● LocalStorage, sessionStorage
●
IndexedDB
●
Animation
● Bezier curve
●
CSS-animations
●
JavaScript animations
● Web components
●
From the orbital height
●
Custom elements
● Shadow DOM
●
Template element
● Shadow DOM slots, composition
● Shadow DOM styling
●
Shadow DOM and events
● Regular expressions
●
Patterns and flags
●
Methods of RegExp and String
● Character classes
●
Escaping, special characters
●
Sets and ranges [...]
●
Quantifiers +, *, ? and {n}
● Greedy and lazy quantifiers
●
Capturing groups
●
Backreferences in pattern: \n and \k
● Alternation (OR) |
●
String start ^ and finish $
●
Multiline mode, flag "m"
● Lookahead and lookbehind
●
Infinite backtracking problem
●
Unicode: flag "u"
● Unicode character properties \p
●
Sticky flag "y", searching at position
Frames and windows
Popups and window methods
A popup window is one of the oldest methods to show additional document to user.
window.open('https://javascript.info/')
…And it will open a new window with given URL. Most modern browsers are
configured to open new tabs instead of separate windows.
Popups exist from really ancient times. The initial idea was to show another content
without closing the main window. As of now, there are other ways to do that: we can
load content dynamically with fetch and show it in a dynamically generated <div> .
So, popups is not something we use everyday.
Also, popups are tricky on mobile devices, that don’t show multiple windows
simultaneously.
Still, there are tasks where popups are still used, e.g. for OAuth authorization (login
with Google/Facebook/…), because:
Popup blocking
In the past, evil sites abused popups a lot. A bad page could open tons of popup
windows with ads. So now most browsers try to block popups and protect the user.
Most browsers block popups if they are called outside of user-triggered event
handlers like onclick .
For example:
// popup blocked
window.open('https://javascript.info');
// popup allowed
button.onclick = () => {
window.open('https://javascript.info');
};
This way users are somewhat protected from unwanted popups, but the functionality
is not disabled totally.
What if the popup opens from onclick , but after setTimeout ? That’s a bit
tricky.
The difference is that Firefox treats a timeout of 2000ms or less are acceptable, but
after it – removes the “trust”, assuming that now it’s “outside of the user action”. So
the first one is blocked, and the second one is not.
window.open
url
An URL to load into the new window.
name
A name of the new window. Each window has a window.name , and here we can
specify which window to use for the popup. If there’s already a window with such
name – the given URL opens in it, otherwise a new window is opened.
params
The configuration string for the new window. It contains settings, delimited by a
comma. There must be no spaces in params, for instance:
width:200,height=100 .
Let’s open a window with minimal set of features just to see which of them browser
allows to disable:
Here most “window features” are disabled and window is positioned offscreen. Run it
and see what really happens. Most browsers “fix” odd things like zero
width/height and offscreen left/top . For instance, Chrome open such a
window with full width/height, so that it occupies the full screen.
Let’s add normal positioning options and reasonable width , height , left ,
top coordinates:
The open call returns a reference to the new window. It can be used to manipulate
it’s properties, change location and even more.
newWin.document.write("Hello, world!");
newWindow.onload = function() {
let html = `<div style="font-size:30px">Welcome!</div>`;
newWindow.document.body.insertAdjacentHTML('afterbegin', html);
};
Please note: immediately after window.open , the new window isn’t loaded yet.
That’s demonstrated by alert in line (*) . So we wait for onload to modify it.
We could also use DOMContentLoaded handler for newWin.document .
⚠ Same origin policy
Windows may freely access content of each other only if they come from the
same origin (the same protocol://domain:port).
Otherwise, e.g. if the main window is from site.com , and the popup from
gmail.com , that’s impossible for user safety reasons. For the details, see
chapter Cross-window communication.
If you run the code below, it replaces the opener (current) window content with
“Test”:
newWin.document.write(
"<script>window.opener.document.body.innerHTML = 'Test'<\/script>"
);
So the connection between the windows is bidirectional: the main window and the
popup have a reference to each other.
Closing a popup
The closed property is true if the window is closed. That’s useful to check if the
popup (or the main window) is still open or not. A user can close it anytime, and our
code should take that possibility into account.
newWindow.onload = function() {
newWindow.close();
alert(newWindow.closed); // true
};
win.moveBy(x,y)
Move the window relative to current position x pixels to the right and y pixels
down. Negative values are allowed (to move left/up).
win.moveTo(x,y)
Move the window to coordinates (x,y) on the screen.
win.resizeBy(width,height)
Resize the window by given width/height relative to the current size. Negative
values are allowed.
win.resizeTo(width,height)
Resize the window to the given size.
⚠ Only popups
To prevent abuse, the browser usually blocks these methods. They only work
reliably on popups that we opened, that have no additional tabs.
⚠ No minification/maximization
JavaScript has no way to minify or maximize a window. These OS-level functions
are hidden from Frontend-developers.
Move/resize methods do not work for maximized/minimized windows.
Scrolling a window
We already talked about scrolling a window in the chapter Window sizes and
scrolling.
win.scrollBy(x,y)
Scroll the window x pixels right and y down relative the current scroll. Negative
values are allowed.
win.scrollTo(x,y)
Scroll the window to the given coordinates (x,y) .
elem.scrollIntoView(top = true)
Scroll the window to make elem show up at the top (the default) or at the bottom for
elem.scrollIntoView(false) .
Focus/blur on a window
In the past evil pages abused those. For instance, look at this code:
When a user attempts to switch out of the window ( blur ), it brings it back to focus.
The intention is to “lock” the user within the window .
So, there are limitations that forbid the code like that. There are many limitations to
protect the user from ads and evils pages. They depend on the browser.
For instance, a mobile browser usually ignores that call completely. Also focusing
doesn’t work when a popup opens in a separate tab rather than a new window.
Still, there are some things that can be done.
For instance:
●
When we open a popup, it’s might be a good idea to run a
newWindow.focus() on it. Just in case, for some OS/browser combinations it
ensures that the user is in the new window now.
●
If we want to track when a visitor actually uses our web-app, we can track
window.onfocus/onblur . That allows us to suspend/resume in-page
activities, animations etc. But please note that the blur event means that the
visitor switched out from the window, but they still may observe it. The window is
in the background, but still may be visible.
Summary
Popup windows are used rarely, as there are alternatives: loading and displaying
information in-page, or in iframe.
If we’re going to open a popup, a good practice is to inform the user about it. An
“opening window” icon near a link or button would allow the visitor to survive the
focus shift and keep both windows in mind.
●
A popup can be opened by the open(url, name, params) call. It returns the
reference to the newly opened window.
●
Browsers block open calls from the code outside of user actions. Usually a
notification appears, so that a user may allow them.
●
Browsers open a new tab by default, but if sizes are provided, then it’ll be a popup
window.
●
The popup may access the opener window using the window.opener property.
●
The main window and the popup can freely read and modify each other if they
havee the same origin. Otherwise, they can change location of each other and
[exchange messages.
To close the popup: use close() call. Also the user may close them (just like any
other windows). The window.closed is true after that.
● Methods focus() and blur() allow to focus/unfocus a window. But they don’t
work all the time.
●
Events focus and blur allow to track switching in and out of the window. But
please note that a window may still be visible even in the background state, after
blur .
Cross-window communication
The “Same Origin” (same site) policy limits access of windows and frames to each
other.
The idea is that if a user has two pages open: one from john-smith.com , and
another one is gmail.com , then they wouldn’t want a script from john-
smith.com to read our mail from gmail.com . So, the purpose of the “Same
Origin” policy is to protect users from information theft.
Same Origin
Two URLs are said to have the “same origin” if they have the same protocol, domain
and port.
These URLs all share the same origin:
●
http://site.com
●
http://site.com/
●
http://site.com/my/page.html
These ones do not:
● http://www.site.com (another domain: www. matters)
●
http://site.org (another domain: .org matters)
●
https://site.com (another protocol: https )
●
http://site.com:8080 (another port: 8080 )
In action: iframe
An <iframe> tag hosts a separate embedded window, with its own separate
document and window objects.
When we access something inside the embedded window, the browser checks if the
iframe has the same origin. If that’s not so then the access is denied (writing to
location is an exception, it’s still permitted).
For instance, let’s try reading and writing to <iframe> from another origin:
<script>
iframe.onload = function() {
// we can get the reference to the inner window
let iframeWindow = iframe.contentWindow; // OK
try {
// ...but not to the document inside it
let doc = iframe.contentDocument; // ERROR
} catch(e) {
alert(e); // Security Error (another origin)
}
// ...we can WRITE into location (and thus load something else into the iframe)
iframe.contentWindow.location = '/'; // OK
iframe.onload = null; // clear the handler, not to run it after the location cha
};
</script>
Contrary to that, if the <iframe> has the same origin, we can do anything with it:
<script>
iframe.onload = function() {
// just do anything
iframe.contentDocument.body.prepend("Hello, world!");
};
</script>
iframe.onload vs iframe.contentWindow.onload
The iframe.onload event (on the <iframe> tag) is essentially the same as
iframe.contentWindow.onload (on the embedded window object). It
triggers when the embedded window fully loads with all resources.
…But we can’t access iframe.contentWindow.onload for an iframe from
another origin, so using iframe.onload .
document.domain = 'site.com';
That’s all. Now they can interact without limitations. Again, that’s only possible for
pages with the same second-level domain.
When an iframe comes from the same origin, and we may access its document ,
there’s a pitfall. It’s not related to cross-domain things, but important to know.
Upon its creation an iframe immediately has a document. But that document is
different from the one that loads into it!
So if we do something with the document immediately, that will probably be lost.
Here, look:
<script>
let oldDoc = iframe.contentDocument;
iframe.onload = function() {
let newDoc = iframe.contentDocument;
// the loaded document is not the same as initial!
alert(oldDoc == newDoc); // false
};
</script>
We shouldn’t work with the document of a not-yet-loaded iframe, because that’s the
wrong document. If we set any event handlers on it, they will be ignored.
<script>
let oldDoc = iframe.contentDocument;
Collection: window.frames
An alternative way to get a window object for <iframe> – is to get it from the
named collection window.frames :
●
By number: window.frames[0] – the window object for the first frame in the
document.
● By name: window.frames.iframeName – the window object for the frame
with name="iframeName" .
For instance:
<script>
alert(iframe.contentWindow == frames[0]); // true
alert(iframe.contentWindow == frames.win); // true
</script>
An iframe may have other iframes inside. The corresponding window objects form
a hierarchy.
For instance:
window.frames[0].parent === window; // true
We can use the top property to check if the current document is open inside a
frame or not:
The sandbox attribute allows for the exclusion of certain actions inside an
<iframe> in order to prevent it executing untrusted code. It “sandboxes” the iframe
by treating it as coming from another origin and/or applying other limitations.
There’s a “default set” of restrictions applied for <iframe sandbox
src="..."> . But it can be relaxed if we provide a space-separated list of
restrictions that should not be applied as a value of the attribute, like this: <iframe
sandbox="allow-forms allow-popups"> .
allow-same-origin
By default "sandbox" forces the “different origin” policy for the iframe. In other
words, it makes the browser to treat the iframe as coming from another origin,
even if its src points to the same site. With all implied restrictions for scripts. This
option removes that feature.
allow-top-navigation
Allows the iframe to change parent.location .
allow-forms
Allows to submit forms from iframe .
allow-scripts
Allows to run scripts from the iframe .
allow-popups
Allows to window.open popups from the iframe
The example below demonstrates a sandboxed iframe with the default set of
restrictions: <iframe sandbox src="..."> . It has some JavaScript and a
form.
Please note that nothing works. So the default set is really harsh:
https://plnkr.co/edit/GAhzx0j3JwAB1TMzwyxL?p=preview
Please note:
The purpose of the "sandbox" attribute is only to add more restrictions. It
cannot remove them. In particular, it can’t relax same-origin restrictions if the
iframe comes from another origin.
Cross-window messaging
The postMessage interface allows windows to talk to each other no matter which
origin they are from.
So, it’s a way around the “Same Origin” policy. It allows a window from john-
smith.com to talk to gmail.com and exchange information, but only if they both
agree and call corresponding JavaScript functions. That makes it safe for users.
postMessage
The window that wants to send a message calls postMessage method of the
receiving window. In other words, if we want to send the message to win , we
should call win.postMessage(data, targetOrigin) .
Arguments:
data
The data to send. Can be any object, the data is cloned using the “structured cloning
algorithm”. IE supports only strings, so we should JSON.stringify complex
objects to support that browser.
targetOrigin
Specifies the origin for the target window, so that only a window from the given origin
will get the message.
<script>
let win = window.frames.example;
win.postMessage("message", "http://example.com");
</script>
<script>
let win = window.frames.example;
win.postMessage("message", "*");
</script>
onmessage
To receive a message, the target window should have a handler on the message
event. It triggers when postMessage is called (and targetOrigin check is
successful).
The event object has special properties:
data
The data from postMessage .
origin
The origin of the sender, for instance http://javascript.info .
source
The reference to the sender window. We can immediately
source.postMessage(...) back if we want.
To assign that handler, we should use addEventListener , a short syntax
window.onmessage does not work.
Here’s an example:
window.addEventListener("message", function(event) {
if (event.origin != 'http://javascript.info') {
// something from an unknown domain, let's ignore it
return;
}
https://plnkr.co/edit/ltrzlGvN8UPdpMtyxlI9?p=preview
There’s no delay
There’s totally no delay between postMessage and the message event. The
event triggers synchronously, faster than setTimeout(...,0) .
Summary
To call methods and access the content of another window, we should first have a
reference to it.
If windows share the same origin (host, port, protocol), then windows can do
whatever they want with each other.
Otherwise, only possible actions are:
●
Change the location of another window (write-only access).
● Post a message to it.
Exceptions are:
●
Windows that share the same second-level domain: a.site.com and
b.site.com . Then setting document.domain='site.com' in both of them
puts them into the “same origin” state.
● If an iframe has a sandbox attribute, it is forcefully put into the “different origin”
state, unless the allow-same-origin is specified in the attribute value. That
can be used to run untrusted code in iframes from the same site.
The postMessage interface allows two windows with any origins to talk:
3. If it is so, then targetWin triggers the message event with special properties:
●
origin – the origin of the sender window (like http://my.site.com )
● source – the reference to the sender window.
● data – the data, any object in everywhere except IE that supports only
strings.
We should use addEventListener to set the handler for this event inside the
target window.
The idea
The demo
Here’s how the evil page looks. To make things clear, the <iframe> is half-
transparent (in real evil pages it’s fully transparent):
<style>
iframe { /* iframe from the victim site */
width: 400px;
height: 100px;
position: absolute;
top:0; left:-20px;
opacity: 0.5; /* in real opacity:0 */
z-index: 1;
}
</style>
<button>Click here!</button>
https://plnkr.co/edit/GQKK8Zc7DXT3KdV7tQUu?p=preview
https://plnkr.co/edit/aebDnhU3B7c6d2QN5Rhy?p=preview
All we need to attack – is to position the <iframe> on the evil page in such a way
that the button is right over the link. So that when a user clicks the link, they actually
click the button. That’s usually doable with CSS.
But then there’s a problem. Everything that the visitor types will be hidden,
because the iframe is not visible.
People will usually stop typing when they can’t see their new characters printing
on the screen.
The oldest defence is a bit of JavaScript which forbids opening the page in a frame
(so-called “framebusting”).
That looks like this:
if (top != window) {
top.location = window.location;
}
That is: if the window finds out that it’s not on top, then it automatically makes itself
the top.
This not a reliable defence, because there are many ways to hack around it. Let’s
cover a few.
Blocking top-navigation
We can block the transition caused by changing top.location in beforeunload
event handler.
The top page (enclosing one, belonging to the hacker) sets a preventing handler to
it, like this:
window.onbeforeunload = function() {
return false;
};
When the iframe tries to change top.location , the visitor gets a message
asking them whether they want to leave.
In most cases the visitor would answer negatively because they don’t know about
the iframe – all they can see is the top page, there’s no reason to leave. So
top.location won’t change!
In action:
https://plnkr.co/edit/WCEMuiV3PmW1klyyf6FH?p=preview
Sandbox attribute
One of the things restricted by the sandbox attribute is navigation. A sandboxed
iframe may not change top.location .
There are other ways to work around that simple protection too.
X-Frame-Options
DENY
Never ever show the page inside a frame.
SAMEORIGIN
Allow inside a frame if the parent document comes from the same origin.
ALLOW-FROM domain
Allow inside a frame if the parent document is from the given domain.
So there are other solutions… For instance, we can “cover” the page with a <div>
with styles height: 100%; width: 100%; , so that it will intercept all clicks.
That <div> is to be removed if window == top or if we figure out that we don’t
need the protection.
Something like this:
<style>
#protector {
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 99999999;
}
</style>
<div id="protector">
<a href="/" target="_blank">Go to the site</a>
</div>
<script>
// there will be an error if top window is from the different origin
// but that's ok here
if (top.document.domain == document.domain) {
protector.remove();
}
</script>
The demo:
https://plnkr.co/edit/WuzXGKQamlVp1sE08svn?p=preview
A cookie with such attribute is only sent to a website if it’s opened directly, not via a
frame, or otherwise. More information in the chapter Cookies, document.cookie.
If the site, such as Facebook, had samesite attribute on its authentication cookie,
like this:
Set-Cookie: authorization=secret; samesite
…Then such cookie wouldn’t be sent when Facebook is open in iframe from another
site. So the attack would fail.
The samesite cookie attribute will not have an effect when cookies are not used.
This may allow other websites to easily show our public, unauthenticated pages in
iframes.
However, this may also allow clickjacking attacks to work in a few limited cases. An
anonymous polling website that prevents duplicate voting by checking IP addresses,
for example, would still be vulnerable to clickjacking because it does not authenticate
users using cookies.
Summary
Clickjacking is a way to “trick” users into clicking on a victim site without even
knowing what’s happening. That’s dangerous if there are important click-activated
actions.
A hacker can post a link to their evil page in a message, or lure visitors to their page
by some other means. There are many variations.
From one perspective – the attack is “not deep”: all a hacker is doing is intercepting
a single click. But from another perspective, if the hacker knows that after the click
another control will appear, then they may use cunning messages to coerce the user
into clicking on them as well.
The attack is quite dangerous, because when we engineer the UI we usually don’t
anticipate that a hacker may click on behalf of the visitor. So vulnerabilities can be
found in totally unexpected places.
● It is recommended to use X-Frame-Options: SAMEORIGIN on pages (or
whole websites) which are not intended to be viewed inside frames.
●
Use a covering <div> if we want to allow our pages to be shown in iframes, but
still stay safe.
This allocates a contiguous memory area of 16 bytes and pre-fills it with zeroes.
ArrayBuffer is a memory area. What’s stored in it? It has no clue. Just a raw
sequence of bytes.
To manipulate an ArrayBuffer , we need to use a “view” object.
A view object does not store anything on it’s own. It’s the “eyeglasses” that give an
interpretation of the bytes stored in the ArrayBuffer .
For instance:
●
Uint8Array – treats each byte in ArrayBuffer as a separate number, with
possible values are from 0 to 255 (a byte is 8-bit, so it can hold only that much).
Such value is called a “8-bit unsigned integer”.
● Uint16Array – treats every 2 bytes as an integer, with possible values from 0
to 65535. That’s called a “16-bit unsigned integer”.
● Uint32Array – treats every 4 bytes as an integer, with possible values from 0
to 4294967295. That’s called a “32-bit unsigned integer”.
● Float64Array – treats every 8 bytes as a floating point number with possible
values from 5.0x10-324 to 1.8x10308 .
ArrayBuffer is the core object, the root of everything, the raw binary data.
But if we’re going to write into it, or iterate over it, basically for almost any operation –
we must use a view, e.g:
TypedArray
The common term for all these views ( Uint8Array , Uint32Array , etc) is
TypedArray . They share the same set of methods and properities.
They are much more like regular arrays: have indexes and iterable.
A typed array constructor (be it Int8Array or Float64Array , doesn’t matter)
behaves differently depending on argument types.
There are 5 variants of arguments:
2. If an Array , or any array-like object is given, it creates a typed array of the same
length and copies the content.
We can use it to pre-fill the array with the data:
4. For a numeric argument length – creates the typed array to contain that many
elements. Its byte length will be length multiplied by the number of bytes in a
single item TypedArray.BYTES_PER_ELEMENT :
Please note, despite of the names like Int8Array , there’s no single-value type
like int , or int8 in JavaScript.
Out-of-bounds behavior
What if we attempt to write an out-of-bounds value into a typed array? There will be
no error. But extra bits are cut-off.
For instance, let’s try to put 256 into Uint8Array . In binary form, 256 is
100000000 (9 bits), but Uint8Array only provides 8 bits per value, that makes
the available range from 0 to 255.
For bigger numbers, only the rightmost (less significant) 8 bits are stored, and the
rest is cut off:
uint8array[0] = 256;
uint8array[1] = 257;
alert(uint8array[0]); // 0
alert(uint8array[1]); // 1
TypedArray methods
These methods allow us to copy typed arrays, mix them, create new arrays from
existing ones, and so on.
DataView
The syntax:
For instance, here we extract numbers in different formats from the same buffer:
dataView.setUint32(0, 0); // set 4-byte number to zero, thus setting all bytes to 0
DataView is great when we store mixed-format data in the same buffer. E.g we
store a sequence of pairs (16-bit integer, 32-bit float). Then DataView allows to
access them easily.
Summary
There are also two additional terms, that are used in descriptions of methods that
operate on binary data:
● ArrayBufferView is an umbrella term for all these kinds of views.
● BufferSource is an umbrella term for ArrayBuffer or
ArrayBufferView .
We’ll see these terms in the next chapters. BufferSource is one of the most
common terms, as it means “any kind of binary data” – an ArrayBuffer or a view
over it.
Here’s a cheatsheet:
✔ Tasks
To solution
●
label – the encoding, utf-8 by default, but big5 , windows-1251 and
many other are also supported.
● options – optional object:
● fatal – boolean, if true then throw an exception for invalid (non-
decodable) characters, otherwise (default) replace them with character
\uFFFD .
● ignoreBOM – boolean, if true then ignore BOM (an optional byte-order
unicode mark), rarely needed.
For instance:
We can decode a part of the buffer by creating a subarray view for it:
let uint8Array = new Uint8Array([0, 72, 101, 108, 108, 111, 0]);
TextEncoder
Blob
ArrayBuffer and views are a part of ECMA standard, a part of JavaScript.
In the browser, there are additional higher-level objects, described in File API , in
particular Blob .
For example:
●
byteStart – the starting byte, by default 0.
● byteEnd – the last byte (exclusive, by default till the end).
● contentType – the type of the new blob, by default the same as the source.
The arguments are similar to array.slice , negative numbers are allowed too.
Blob as URL
A Blob can be easily used as an URL for <a> , <img> or other tags, to show its
contents.
Thanks to type , we can allso download/upload blobs, and it naturally becomes
Content-Type in network requests.
Let’s start with a simple example. By clicking on a link you download a dynamically-
generated blob with hello world contents as a file:
<!-- download attribute forces the browser to download instead of navigating -->
<a download="hello.txt" href='#' id="link">Download</a>
<script>
let blob = new Blob(["Hello, world!"], {type: 'text/plain'});
link.href = URL.createObjectURL(blob);
</script>
Here’s the similar code that causes user to download the dynamicallly created Blob,
without any HTML:
link.href = URL.createObjectURL(blob);
link.click();
URL.revokeObjectURL(link.href);
URL.createObjectURL takes a blob and creates an unique URL for it, in the
form blob:<origin>/<uuid> .
blob:https://javascript.info/1e67e00e-860d-40a5-89ae-6ab0cbee6273
The browser for each url generated by URL.createObjectURL stores an the url
→ blob mapping internally. So such urls are short, but allow to access the blob.
A generated url (and hence the link with it) is only valid within the current document,
while it’s open. And it allows to reference the blob in <img> , <a> , basically any
other object that expects an url.
There’s a side-effect though. While there’s an mapping for a blob, the blob itself
resides in the memory. The browser can’t free it.
The mapping is automatically cleared on document unload, so blobs are freed then.
But if an app is long-living, then that doesn’t happen soon.
So if we create an URL, that blob will hang in memory, even if not needed any
more.
URL.revokeObjectURL(url) removes the reference from the internal mapping,
thus allowing the blob to be deleted (if there are no other references), and the
memory to be freed.
In the last example, we intend the blob to be used only once, for instant
downloading, so we call URL.revokeObjectURL(link.href) immediately.
In the previous example though, with the clickable HTML-link, we don’t call
URL.revokeObjectURL(link.href) , because that would make the blob url
invalid. After the revocation, as the mapping is removed, the url doesn’t work any
more.
Blob to base64
<img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5
The browser will decode the string and show the image:
To transform a blob into base64, we’ll use the built-in FileReader object. It can
read data from Blobs in multiple formats. In the next chapter we’ll cover it more in-
depth.
Here’s the demo of downloading a blob, now via base-64:
reader.onload = function() {
link.href = reader.result; // data url
link.click();
};
Image to blob
We can create a blob of an image, an image part, or even make a page screenshot.
That’s handy to upload it somewhere.
Image operations are done via <canvas> element:
In the example below, an image is just copied, but we could cut from it, or transform
it on canvas prior to making a blob:
link.href = URL.createObjectURL(blob);
link.click();
// delete the internal blob reference, to let the browser clear memory from it
URL.revokeObjectURL(link.href);
}, 'image/png');
The Blob constructor allows to create a blob from almost anything, including any
BufferSource .
fileReader.readAsArrayBuffer(blob);
fileReader.onload = function(event) {
let arrayBuffer = fileReader.result;
};
Summary
While ArrayBuffer , Uint8Array and other BufferSource are “binary data”,
a Blob represents “binary data with type”.
That makes Blobs convenient for upload/download operations, that are so common
in the browser.
Methods that perform web-requests, such as XMLHttpRequest, fetch and so on, can
work with Blob natively, as well as with other binary types.
We can easily convert betweeen Blob and low-level binary data types:
●
We can make a Blob from a typed array using new Blob(...) constructor.
● We can get back ArrayBuffer from a Blob using FileReader , and then
create a view over it for low-level binary processing.
As File inherits from Blob , File objects have the same properties, plus:
● name – the file name,
● lastModified – the timestamp of last modification.
<script>
function showFile(input) {
let file = input.files[0];
Please note:
The input may select multiple files, so input.files is an array-like object with
them. Here we have only one file, so we just take input.files[0] .
FileReader
FileReader is an object with the sole purpose of reading data from Blob (and
hence File too) objects.
It delivers the data using events, as reading from disk may take time.
The constructor:
The choice of read* method depends on which format we prefer, how we’re going
to use the data.
● readAsArrayBuffer – for binary files, to do low-level binary operations. For
high-level operations, like slicing, File inherits from Blob , so we can call them
directly, without reading.
●
readAsText – for text files, when we’d like to get a string.
● readAsDataURL – when we’d like to use this data in src for img or another
tag. There’s an alternative to reading a file for that, as discussed in chapter Blob:
URL.createObjectURL(file) .
As the reading proceeds, there are events:
●
loadstart – loading started.
● progress – occurs during reading.
●
load – no errors, reading complete.
● abort – abort() called.
● error – error has occurred.
●
loadend – reading finished with either success or failure.
The most widely used events are for sure load and error .
<script>
function readFile(input) {
let file = input.files[0];
reader.readAsText(file);
reader.onload = function() {
console.log(reader.result);
};
reader.onerror = function() {
console.log(reader.error);
};
}
</script>
FileReader for blobs
As mentioned in the chapter Blob, FileReader can read not just files, but any
blobs.
We can use it to convert a blob to another format:
● readAsArrayBuffer(blob) – to ArrayBuffer ,
● readAsText(blob, [encoding]) – to string (an alternative to
TextDecoder ),
● readAsDataURL(blob) – to base64 data url.
Its reading methods read* do not generate events, but rather return a result,
as regular functions do.
That’s only inside a Web Worker though, because delays in synchronous calls,
that are possible while reading from files, in Web Workers are less important.
They do not affect the page.
Summary
In addition to Blob methods and properties, File objects also have name and
lastModified properties, plus the internal ability to read from filesystem. We
usually get File objects from user input, like <input> or Drag’n’Drop events
( ondragend ).
FileReader objects can read from a file or a blob, in one of three formats:
● String ( readAsText ).
● ArrayBuffer ( readAsArrayBuffer ).
● Data url, base-64 encoded ( readAsDataURL ).
In many cases though, we don’t have to read the file contents. Just as we did with
blobs, we can create a short url with URL.createObjectURL(file) and assign
it to <a> or <img> . This way the file can be downloaded or shown up as an image,
as a part of canvas etc.
And if we’re going to send a File over a network, that’s also easy: network API like
XMLHttpRequest or fetch natively accepts File objects.
Network requests
Fetch
JavaScript can send network requests to the server and load new information
whenever is needed.
For example, we can:
● Submit an order,
● Load user information,
● Receive latest updates from the server,
● …etc.
The browser starts the request right away and returns a promise .
First, the promise resolves with an object of the built-in Response class
as soon as the server responds with headers.
So we can check HTTP status, to see whether it is successful or not, check headers,
but don’t have the body yet.
The promise rejects if the fetch was unable to make HTTP-request, e.g. network
problems, or there’s no such site. HTTP-errors, even such as 404 or 500, are
considered a normal flow.
We can see them in response properties:
● ok – boolean, true if the HTTP status code is 200-299.
● status – HTTP status code.
For example:
Second, to get the response body, we need to use an additional method call.
Response provides multiple promise-based methods to access the body in various
formats:
● response.json() – parse the response as JSON object,
● response.text() – return the response as text,
● response.formData() – return the response as FormData object
(form/multipart encoding, explained in the next chapter),
● response.blob() – return the response as Blob (binary data with type),
● response.arrayBuffer() – return the response as ArrayBuffer (pure binary
data),
● additionally, response.body is a ReadableStream object, it allows to read
the body chunk-by-chunk, we’ll see an example later.
For instance, let’s get a JSON-object with latest commits from GitHub:
let commits = await response.json(); // read response body and parse as JSON
alert(commits[0].author.login);
As a show-case for reading in binary format, let’s fetch and show an image (see
chapter Blob for details about operations on blobs):
// show it
img.src = URL.createObjectURL(blob);
⚠ Important:
We can choose only one body-parsing method.
If we got the response with response.text() , then response.json()
won’t work, as the body content has already been processed.
These headers ensure proper and safe HTTP, so they are controlled exclusively by
the browser.
POST requests
let user = {
name: 'John',
surname: 'Smith'
};
Sending an image
For example, here’s a <canvas> where we can draw by moving a mouse. A click
on the “submit” button sends the image to server:
<body style="margin:0">
<canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>
<script>
canvasElem.onmousemove = function(e) {
let ctx = canvasElem.getContext('2d');
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
};
</script>
</body>
Submit
Here we also didn’t need to set Content-Type manually, because a Blob object
has a built-in type (here image/png , as generated by toBlob ).
function submit() {
canvasElem.toBlob(function(blob) {
fetch('/article/fetch/post/image', {
method: 'POST',
body: blob
})
.then(response => response.json())
.then(result => alert(JSON.stringify(result, null, 2)))
}, 'image/png');
}
Summary
Or, promise-style:
fetch(url, options)
.then(response => response.json())
.then(result => /* process result */)
Response properties:
● response.status – HTTP code of the response,
● response.ok – true is the status is 200-299.
● response.headers – Map-like object with HTTP headers.
✔ Tasks
The GitHub url with user informaiton for the given USERNAME is:
https://api.github.com/users/USERNAME .
Important details:
1. There should be one fetch request per user. And requests shouldn’t wait for
each other. So that the data arrives as soon as possible.
2. If any request fails, or if there’s no such user, the function should return null
in the resulting array.
To solution
FormData
This chapter is about sending HTML forms: with or without files, with additional fields
and so on. FormData objects can help with that.
If HTML form element is provided, it automatically captures its fields. As you may
have already guessed, FormData is an object to store and send form data.
The special thing about FormData is that network methods, such as fetch , can
accept a FormData object as a body. It’s encoded and sent out with Content-
Type: form/multipart . So, from the server point of view, that looks like a usual
form submission.
Sending a simple form
<form id="formElem">
<input type="text" name="name" value="John">
<input type="text" name="surname" value="Smith">
<input type="submit">
</form>
<script>
formElem.onsubmit = async (e) => {
e.preventDefault();
alert(result.message);
};
</script>
Here, the server accepts the POST request with the form and replies “User saved”.
FormData Methods
There’s also method set , with the same syntax as append . The difference is that
.set removes all fields with the given name , and then appends a new field. So it
makes sure there’s only field with such name :
● formData.set(name, value) ,
● formData.set(name, blob, fileName) .
<form id="formElem">
<input type="text" name="firstName" value="John">
Picture: <input type="file" name="picture" accept="image/*">
<input type="submit">
</form>
<script>
formElem.onsubmit = async (e) => {
e.preventDefault();
alert(result.message);
};
</script>
As we’ve seen in the chapter Fetch, sending a dynamically generated Blob , e.g. an
image, is easy. We can supply it directly as fetch parameter body .
In practice though, it’s often convenient to send an image not separately, but as a
part of the form, with additional fields, such as “name” and other metadata.
Also, servers are usually more suited to accept multipart-encoded forms, rather than
raw binary data.
This example submits an image from <canvas> , along with some other fields,
using FormData :
<body style="margin:0">
<canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>
<script>
canvasElem.onmousemove = function(e) {
let ctx = canvasElem.getContext('2d');
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
};
</script>
</body>
Submit
Summary
FormData objects are used to capture HTML form and submit it using fetch or
another network method.
We can either create new FormData(form) from an HTML form, or create an
empty object, and then append fields with methods:
● formData.append(name, value)
● formData.append(name, blob, fileName)
● formData.set(name, value)
● formData.set(name, blob, fileName)
That’s it!
if (done) {
break;
}
To log the progress, we just need for every value add its length to the counter.
Here’s the full code to get response and log the progress, more explanations follow:
if (done) {
break;
}
chunks.push(value);
receivedLength += value.length;
// We're done!
let commits = JSON.parse(result);
alert(commits[0].author.login);
Please note, we can’t use both these methods to read the same response. Either
use a reader or a response method to get the result.
2. Prior to reading, we can figure out the full response length from the Content-
Length header.
We gather response chunks in the array. That’s important, because after the
response is consumed, we won’t be able to “re-read” it using
response.json() or another way (you can try, there’ll be an error).
To create a string, we need to interpret these bytes. The built-in TextDecoder does
exactly that. Then we can JSON.parse it.
What if we need binary content instead of JSON? That’s even simpler. Replace
steps 4 and 5 with a single call to a blob from all chunks:
At we end we have the result (as a string or a blob, whatever is convenient), and
progress-tracking in the process.
Once again, please note, that’s not for upload progress (no way now with fetch ),
only for download progress.
Fetch: Abort
Aborting a fetch is a little bit tricky. Remember, fetch returns a promise. And
JavaScript generally has no concept of “aborting” a promise. So how can we cancel
a fetch?
There’s a special built-in object for such purposes: AbortController .
Like this:
controller.abort(); // abort!
controller.abort();
We’re done: fetch gets the event from signal and aborts the request.
When a fetch is aborted, its promise rejects with an error named AbortError , so
we should handle it:
// abort in 1 second
let controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
try {
let response = await fetch('/article/fetch-abort/demo/hang', {
signal: controller.signal
});
} catch(err) {
if (err.name == 'AbortError') { // handle abort()
alert("Aborted!");
} else {
throw err;
}
}
For instance, here we fetch many urls in parallel, and the controller aborts them
all:
let urls = [...]; // a list of urls to fetch in parallel
// from elsewhere:
// controller.abort() stops all fetches
If we have our own jobs, different from fetch , we can use a single
AbortController to stop those, together with fetches.
// from elsewhere:
// controller.abort() stops all fetches and ourJob
try {
await fetch('http://example.com');
} catch(err) {
alert(err); // Failed to fetch
}
Using forms
One way to communicate with another server was to submit a <form> there.
People submitted it into <iframe> , just to stay on the current page, like this:
So, it was possible to make a GET/POST request to another site, even without
networking methods. But as it’s forbidden to access the content of an <iframe>
from another site, it wasn’t possible to read the response.
As we can see, forms allowed to send data anywhere, but not receive the response.
To be precise, there wre actually tricks for that (required special scripts at both the
iframe and the page), but let these dinosaurs rest in peace.
Using scripts
Another trick was to use a <script src="http://another.com/…"> tag. A
script could have any src , from any domain. But again – it was impossible to
access the raw content of such script.
If another.com intended to expose data for this kind of access, then a so-called
“JSONP (JSON with padding)” protocol was used.
Let’s say we need to get the data from http://another.com this way:
4. When the remote script loads and executes, gotWeather runs, and, as it’s our
function, we have the data.
That works, and doesn’t violate security, because both sides agreed to pass the data
this way. And, when both sides agree, it’s definitely not a hack. There are still
services that provide such access, as it works even for very old browsers.
After a while, networking methods appeared, such as XMLHttpRequest .
Simple Requests are, well, simpler to make, so let’s start with them.
A simple request is a request that satisfies two conditions:
Any other request is considered “non-simple”. For instance, a request with PUT
method or with an API-Key HTTP-header does not fit the limitations.
So, even a very old server should be ready to accept a simple request.
Contrary to that, requests with non-standard headers or e.g. method DELETE can’t
be created this way. For a long time JavaScript was unable to do such requests. So
an old server may assume that such requests come from a privileged source,
“because a webpage is unable to send them”.
When we try to make a non-simple request, the browser sends a special “preflight”
request that asks the server – does it agree to accept such cross-origin requests, or
not?
And, unless the server explicitly confirms that with headers, a non-simple request is
not sent.
Now we’ll go into details. All of them serve a single purpose – to ensure that new
cross-origin capabilities are only accessible with an explicit permission from the
server.
GET /request
Host: anywhere.com
Origin: https://javascript.info
...
As you can see, Origin contains exactly the origin (domain/protocol/port), without
a path.
The server can inspect the Origin and, if it agrees to accept such a request, adds
a special header Access-Control-Allow-Origin to the response. That
header should contain the allowed origin (in our case
https://javascript.info ), or a star * . Then the response is successful,
otherwise an error.
The browser plays the role of a trusted mediator here:
1. It ensures that the corrent Origin is sent with a cross-domain request.
2. If checks for correct Access-Control-Allow-Origin in the response, if it is
so, then JavaScript access, otherwise forbids with an error.
200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: https://javascript.info
Response headers
For cross-origin request, by default JavaScript may only access “simple response
headers”:
● Cache-Control
● Content-Language
● Content-Type
● Expires
●
Last-Modified
● Pragma
This header contains the full response length. So, if we’re downloading
something and would like to track the percentage of progress, then an additional
permission is required to access that header (see below).
To grant JavaScript access to any other response header, the server must list it in
the Access-Control-Expose-Headers header.
For example:
200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Expose-Headers: Content-Length,API-Key
“Non-simple” requests
We can use any HTTP-method: not just GET/POST , but also PATCH , DELETE and
others.
Some time ago no one could even assume that a webpage is able to do such
requests. So there may exist webservices that treat a non-standard method as a
signal: “That’s not a browser”. They can take it into account when checking access
rights.
So, to avoid misunderstandings, any “non-simple” request – that couldn’t be done in
the old times, the browser does not make such requests right away. Before it sends
a preliminary, so-called “preflight” request, asking for permission.
A preflight request uses method OPTIONS and has no body.
● Access-Control-Request-Method header has the requested method.
● Access-Control-Request-Headers header provides a comma-separated
list of non-simple HTTP-headers.
If the server agrees to serve the requests, then it should respond with status 200,
without body.
● The response header Access-Control-Allow-Methods must have the
allowed method.
● The response header Access-Control-Allow-Headers must have a list of
allowed headers.
● Additionally, the header Access-Control-Max-Age may specify a number of
seconds to cache the permissions. So the browser won’t have to send a preflight
for subsequent requests that satisfy given permissions.
Let’s see how it works step-by-step on example, for a cross-domain PATCH request
(this method is often used to update data):
There are three reasons why the request is not simple (one is enough):
● Method PATCH
● Content-Type is not one of: application/x-www-form-urlencoded ,
multipart/form-data , text/plain .
● “Non-simple” API-Key header.
OPTIONS /service.json
Host: site.com
Origin: https://javascript.info
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: Content-Type,API-Key
● Method: OPTIONS .
● The path – exactly the same as the main request: /service.json .
● Cross-origin special headers:
●
Origin – the source origin.
● Access-Control-Request-Method – requested method.
●
Access-Control-Request-Headers – a comma-separated list of “non-
simple” headers.
200 OK
Access-Control-Allow-Methods: PUT,PATCH,DELETE
Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control
Access-Control-Max-Age: 86400
Now the browser can see that PATCH is in the list of allowed methods, and both
headers are in the list too, so it sends out the main request.
Besides, the preflight response is cached for time, specified by Access-Control-
Max-Age header (86400 seconds, one day), so subsequent requests will not cause
a preflight. Assuming that they fit the allowances, they will be sent directly.
PATCH /service.json
Host: site.com
Content-Type: application/json
API-Key: secret
Origin: https://javascript.info
Access-Control-Allow-Origin: https://javascript.info
Credentials
A cross-origin request by default does not bring any credentials (cookies or HTTP
authentication).
That’s uncommon for HTTP-requests. Usually, a request to http://site.com is
accompanied by all cookies from that domain. But cross-domain requests made by
JavaScript methods are an exception.
For example, fetch('http://another.com') does not send any cookies,
even those that belong to another.com domain.
Why?
That’s because a request with credentials is much more powerful than an
anonymous one. If allowed, it grants JavaScript the full power to act and access
sensitive information on behalf of a user.
Does the server really trust pages from Origin that much? Then it must explicitly
allow requests with credentials with an additional header.
To send credentials, we need to add the option credentials: "include" , like
this:
fetch('http://another.com', {
credentials: "include"
});
Now fetch sends cookies originating from another.com with out request to that
site.
If the server wishes to accept the request with credentials, it should add a header
Access-Control-Allow-Credentials: true to the response, in addition to
Access-Control-Allow-Origin .
For example:
200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Credentials: true
Summary
Networking methods split cross-origin requests into two kinds: “simple” and all the
others.
Simple requests must satisfy the following conditions:
● Method: GET, POST or HEAD.
● Headers – we can set only:
● Accept
● Accept-Language
● Content-Language
● Content-Type to the value application/x-www-form-urlencoded ,
multipart/form-data or text/plain .
The essential difference is that simple requests were doable since ancient times
using <form> or <script> tags, while non-simple were impossible for browsers
for a long time.
So, practical difference is that simple requests are sent right away, with Origin
header, but for other ones the browser makes a preliminary “preflight” request,
asking for permission.
For simple requests:
● → The browser sends Origin header with the origin.
● ← For requests without credentials (not sent default), the server should set:
●
Access-Control-Allow-Origin to * or same value as Origin
● ← For requests with credentials, the server should set:
● Access-Control-Allow-Origin to same value as Origin
● Access-Control-Allow-Credentials to true
✔ Tasks
As you probably know, there’s HTTP-header Referer , that usually contains an url
of the page which initiated a network request.
The questions:
To solution
Fetch API
So far, we know quite a bit about fetch .
Now let’s see the rest of API, to cover all its abilities.
Here’s the full list of all possible fetch options with their default values
(alternatives in comments):
referrer, referrerPolicy
That header contains the url of the page that made the request. In most scenarios, it
plays a very minor informational role, but sometimes, for security purposes, it makes
sense to remove or shorten it.
The referrer option allows to set any Referer within the current origin) or
disable it.
To send no referer, set an empty string:
fetch('/page', {
referrer: "" // no Referer header
});
fetch('/page', {
// assuming we're on https://javascript.info
// we can set any Referer header, but only within the current origin
referrer: "https://javascript.info/anotherpage"
});
Let’s say we have an admin zone with URL structure that shouldn’t be known from
outside of the site.
If we send a cross-origin fetch , then by default it sends the Referer header with
the full url of our page (except when we request from HTTPS to HTTP, then no
Referer ).
fetch('https://another.com/page', {
referrerPolicy: "no-referrer" // no Referer, same effect as referrer: ""
});
Otherwise, if we’d like the remote side to see only the domain where the request
comes from, but not the full URL, we can send only the “origin” part of it:
fetch('https://another.com/page', {
referrerPolicy: "strict-origin" // Referer: https://javascript.info
});
mode
That may be useful in contexts when the fetch url comes from 3rd-party, and we
want a “power off switch” to limit cross-origin capabilities.
credentials
The credentials option specifies whether fetch should send cookies and
HTTP-Authorization headers with the request.
● "same-origin" – the default, don’t send for cross-origin requests,
● "include" – always send, requires Accept-Control-Allow-
Credentials from cross-origin server,
● "omit" – never send, even for same-origin requests.
cache
By default, fetch requests make use of standard HTTP-caching. That is, it honors
Expires , Cache-Control headers, sends If-Modified-Since , and so on.
Just like regular HTTP-requests do.
The cache options allows to ignore HTTP-cache or fine-tune its usage:
● "default" – fetch uses standard HTTP-cache rules and headers;
● "no-store" – totally ignore HTTP-cache, this mode becomes the default if we
set a header If-Modified-Since , If-None-Match , If-Unmodified-
Since , If-Match , or If-Range ;
● "reload" – don’t take the result from HTTP-cache (if any), but populate cache
with the response (if response headers allow);
● "no-cache" – create a conditional request if there is a cached response, and a
normal request otherwise. Populate HTTP-cache with the response;
● "force-cache" – use a response from HTTP-cache, even if it’s stale. If there’s
no response in HTTP-cache, make a regular HTTP-request, behave normally;
● "only-if-cached" – use a response from HTTP-cache, even if it’s stale. If
there’s no response in HTTP-cache, then error. Only works when mode is
"same-origin" .
redirect
integrity
The integrity option allows to check if the response matches the known-ahead
checksum.
As described in the specification , supported hash-functions are SHA-256, SHA-
384, and SHA-512, there might be others depending on a browser.
For example, we’re downloading a file, and we know that it’s SHA-256 checksum is
“abc” (a real checksum is longer, of course).
We can put it in the integrity option, like this:
fetch('http://site.com/file', {
integrity: 'sha256-abd'
});
Then fetch will calculate SHA-256 on its own and compare it with our string. In
case of a mismatch, an error is triggered.
keepalive
The keepalive option indicates that the request may outlive the page.
For example, we gather statistics about how the current visitor uses our page
(mouse clicks, page fragments he views), to improve user experience.
When the visitor leaves our page – we’d like to save it on our server.
We can use window.onunload for that:
window.onunload = function() {
fetch('/analytics', {
method: 'POST',
body: "statistics",
keepalive: true
});
};
URL objects
The built-in URL class provides a convenient interface for creating and parsing
URLs.
There are no networking methods that require exactly an URL object, strings are
good enough. So technically we don’t have to use URL . But sometimes it can be
really helpful.
Creating an URL
● url – the URL string or path (if base is set, see below).
● base – an optional base, if set and url has only path, then the URL is
generated relative to base .
alert(url1); // https://javascript.info/profile/admin
alert(url2); // https://javascript.info/profile/admin
alert(testerUrl); // https://javascript.info/profile/tester
The URL object immediately allows us to access its components, so it’s a nice way
to parse the url, e.g.:
alert(url.protocol); // https:
alert(url.host); // javascript.info
alert(url.pathname); // /url
SearchParams “?…”
Let’s say we want to create an url with given search params, for instance,
https://google.com/search?query=JavaScript .
…But parameters need to be encoded if they contain spaces, non-latin letters, etc
(more about that below).
So there’s URL property for that: url.searchParams , an object of type
URLSearchParams .
For example:
alert(url); // https://google.com/search?query=test+me%21
alert(url); // https://google.com/search?q=test+me%21&tbs=qdr%3Ay
Encoding
There’s a standard RFC3986 that defines which characters are allowed and
which are not.
Those that are not allowed, must be encoded, for instance non-latin letters and
spaces – replaced with their UTF-8 codes, prefixed by % , such as %20 (a space
can be encoded by + , for historical reasons that’s allowed in URL too).
The good news is that URL objects handle all that automatically. We just supply all
parameters unencoded, and then convert the URL to the string:
url.searchParams.set('key', 'ъ');
alert(url); //https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%81%D1%82?key=%D1%8A
As you can see, both Тест in the url path and ъ in the parameter are encoded.
Encoding strings
If we’re using strings instead of URL objects, then we can encode manually using
built-in functions:
● encodeURI – encode URL as a whole.
●
encodeURI – decode it back.
●
encodeURIComponent – encode URL components, such as search
parameters, or a hash, or a pathname.
● decodeURIComponent – decodes it back.
That’s easy to understand if we look at the URL, that’s split into components in the
picture above:
http://site.com:8080/path/page?p1=v1&p2=v2#hash
…On the other hand, if we look at a single URL component, such as a search
parameter, we should encode more characters, e.g. ? , = and & are used for
formatting.
That’s what encodeURIComponent does. It encodes same characters as
encodeURI , plus a lot of others, to make the resulting value safe to use in any URL
component.
For example:
As we can see, encodeURI does not encode & , as this is a legit character in URL
as a whole.
But we should encode & inside a search parameter, otherwise, we get
q=Rock&Roll – that is actually q=Rock plus some obscure parameter Roll .
Not as intended.
So we should use only encodeURIComponent for each search parameter, to
correctly insert it in the URL string. The safest is to encode both name and value,
unless we’re absolutely sure that either has only allowed characters.
Why URL?
Lots of old code uses these functions, these are sometimes convenient, and by noo
means not dead.
But in modern code, it’s recommended to use classes URL and
URLSearchParams .
One of the reason is: they are based on the recent URI spec: RFC3986 , while
encode* functions are based on the obsolete version RFC2396 .
As we can see, encodeURI replaced square brackets [...] , that’s not correct,
the reason is: IPv6 urls did not exist at the time of RFC2396 (August 1998).
Such cases are rare, encode* functions work well most of the time, it’s just one of
the reason to prefer new APIs.
XMLHttpRequest
XMLHttpRequest is a built-in browser object that allows to make HTTP requests
in JavaScript.
Despite of having the word “XML” in its name, it can operate on any data, not only in
XML format. We can upload/download files, track progress and much more.
Right now, there’s another, more modern method fetch , that somewhat
deprecates XMLHttpRequest .
Does that sound familiar? If yes, then all right, go on with XMLHttpRequest .
Otherwise, please head on to Fetch.
The basics
1. Create XMLHttpRequest :
2. Initialize it:
Please note that open call, contrary to its name, does not open the connection. It
only configures the request, but the network activity only starts with the call of
send .
3. Send it out.
xhr.send([body])
This method opens the connection and sends the request to server. The optional
body parameter contains the request body.
Some request methods like GET do not have a body. And some of them like
POST use body to send the data to the server. We’ll see examples later.
xhr.onload = function() {
alert(`Loaded: ${xhr.status} ${xhr.response}`);
};
xhr.onprogress = function(event) {
if (event.lengthComputable) {
alert(`Received ${event.loaded} of ${event.total} bytes`);
} else {
alert(`Received ${event.loaded} bytes`); // no Content-Length
}
};
xhr.onerror = function() {
alert("Request failed");
};
Once the server has responded, we can receive the result in the following properties
of the request object:
status
HTTP status code (a number): 200 , 404 , 403 and so on, can be 0 in case of a
non-HTTP failure.
statusText
HTTP status message (a string): usually OK for 200 , Not Found for 404 ,
Forbidden for 403 and so on.
If the request does not succeed within the given time, it gets canceled and
timeout event triggers.
Response Type
xhr.responseType = 'json';
xhr.send();
Please note:
In the old scripts you may also find xhr.responseText and even
xhr.responseXML properties.
They exist for historical reasons, to get either a string or XML document.
Nowadays, we should set the format in xhr.responseType and get
xhr.response as demonstrated above.
Ready states
xhr.onreadystatechange = function() {
if (xhr.readyState == 3) {
// loading
}
if (xhr.readyState == 4) {
// request finished
}
};
You can find readystatechange listeners in really old code, it’s there for
historical reasons, as there was a time when there were no load and other events.
Aborting request
We can terminate the request at any time. The call to xhr.abort() does that:
Synchronous requests
If in the open method the third parameter async is set to false , the request is
made synchronously.
In other words, JavaScript execution pauses at send() and resumes when the
response is received. Somewhat like alert or prompt commands.
try {
xhr.send();
if (xhr.status != 200) {
alert(`Error ${xhr.status}: ${xhr.statusText}`);
} else {
alert(xhr.response);
}
} catch(err) { // instead of onerror
alert("Request failed");
}
It might look good, but synchronous calls are used rarely, because they block in-
page JavaScript till the loading is complete. In some browsers it becomes impossible
to scroll. If a synchronous call takes too much time, the browser may suggest to
close the “hanging” webpage.
Many advanced capabilities of XMLHttpRequest , like requesting from another
domain or specifying a timeout, are unavailable for synchronous requests. Also, as
you can see, no progress indication.
Because of all that, synchronous requests are used very sparingly, almost never. We
won’t talk about them any more.
HTTP-headers
XMLHttpRequest allows both to send custom headers and read headers from the
response.
There are 3 methods for HTTP-headers:
setRequestHeader(name, value)
Sets the request header with the given name and value .
For instance:
xhr.setRequestHeader('Content-Type', 'application/json');
⚠ Headers limitations
Several headers are managed exclusively by the browser, e.g. Referer and
Host . The full list is in the specification .
XMLHttpRequest is not allowed to change them, for the sake of user safety
and correctness of the request.
Once the header is set, it’s set. Additional calls add information to the header,
don’t overwrite it.
For instance:
xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');
For instance:
xhr.getResponseHeader('Content-Type')
getAllResponseHeaders()
Returns all response headers, except Set-Cookie and Set-Cookie2 .
Cache-Control: max-age=31536000
Content-Length: 4260
Content-Type: image/png
Date: Sat, 08 Sep 2012 16:53:16 GMT
The line break between headers is always "\r\n" (doesn’t depend on OS), so we
can easily split it into individual headers. The separator between the name and the
value is always a colon followed by a space ": " . That’s fixed in the specification.
So, if we want to get an object with name/value pairs, we need to throw in a bit JS.
Like this (assuming that if two headers have the same name, then the latter one
overwrites the former one):
POST, FormData
The syntax:
let formData = new FormData([form]); // creates an object, optionally fill from <for
formData.append(name, value); // appends a field
We create it, optionally from a form, append more fields if needed, and then:
For instance:
<form name="person">
<input name="name" value="John">
<input name="surname" value="Smith">
</form>
<script>
// pre-fill FormData from the form
let formData = new FormData(document.forms.person);
// send it out
let xhr = new XMLHttpRequest();
xhr.open("POST", "/article/xmlhttprequest/post/user");
xhr.send(formData);
</script>
xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
xhr.send(json);
The .send(body) method is pretty omnivore. It can send almost everything,
including Blob and BufferSource objects.
Upload progress
That is: if we POST something, XMLHttpRequest first uploads our data (the
request body), then downloads the response.
If we’re uploading something big, then we’re surely more interested in tracking the
upload progress. But xhr.onprogress doesn’t help here.
Example of handlers:
xhr.upload.onprogress = function(event) {
alert(`Uploaded ${event.loaded} of ${event.total} bytes`);
};
xhr.upload.onload = function() {
alert(`Upload finished successfully.`);
};
xhr.upload.onerror = function() {
alert(`Error during the upload: ${xhr.status}`);
};
xhr.open("POST", "/article/xmlhttprequest/post/upload");
xhr.send(file);
}
</script>
Cross-origin requests
XMLHttpRequest can make cross-domain requests, using the same CORS policy
as fetch.
Just like fetch , it doesn’t send cookies and HTTP-authorization to another origin
by default. To enable them, set xhr.withCredentials to true :
xhr.open('POST', 'http://anywhere.com/request');
...
See the chapter Fetch: Cross-Origin Requests for details about cross-origin headers.
Summary
xhr.open('GET', '/my/url');
xhr.send();
xhr.onload = function() {
if (xhr.status != 200) { // HTTP error?
// handle error
alert( 'Error: ' + xhr.status);
return;
}
xhr.onprogress = function(event) {
// report progress
alert(`Loaded ${event.loaded} of ${event.total}`);
};
xhr.onerror = function() {
// handle non-HTTP error (e.g. network down)
};
There are actually more events, the modern specification lists them (in the
lifecycle order):
● loadstart – the request has started.
● progress – a data packet of the response has arrived, the whole response
body at the moment is in responseText .
● abort – the request was canceled by the call xhr.abort() .
● error – connection error has occurred, e.g. wrong domain name. Doesn’t
happen for HTTP-errors like 404.
●
load – the request has finished successfully.
●
timeout – the request was canceled due to timeout (only happens if it was set).
●
loadend – triggers after load , error , timeout or abort .
The error , abort , timeout , and load events are mutually exclusive. Only
one of them may happen.
The most used events are load completion ( load ), load failure ( error ), or we can
use a single loadend handler and check the response to see what happened.
How to resume the upload after lost connection? There’s no built-in option for that,
but we have the pieces to implement it.
Resumable uploads should come with upload progress indication, as we expect big
files (if we may need to resume). So, as fetch doesn’t allow to track upload
progress, we’ll use XMLHttpRequest.
To resume upload, we need to know how much was uploaded till the connection was
lost.
There’s xhr.upload.onprogress to track upload progress.
Unfortunately, it’s useless here, as it triggers when the data is sent, but was it
received by the server? The browser doesn’t know.
Maybe it was buffered by a local network proxy, or maybe the remote server process
just died and couldn’t process them, or it was just lost in the middle when the
connection broke, and didn’t reach the receiver.
So, this event is only useful to show a nice progress bar.
To resume upload, we need to know exactly the number of bytes received by the
server. And only the server can tell that.
Algorithm
1. First, we create a file id, to uniquely identify the file we’re uploading, e.g.
That’s needed for resume upload, to tell the server what we’re resuming.
2. Send a request to the server, asking how many bytes it already has, like this:
This assumes that the server tracks file uploads by X-File-Id header. Should
be implemented at server-side.
3. Then, we can use Blob method slice to send the file from startByte :
// send file id, so that the server knows which file to resume
xhr.setRequestHeader('X-File-Id', fileId);
// send the byte we're resuming from, so the server knows we're resuming
xhr.setRequestHeader('X-Start-Byte', startByte);
Here we send the server both file id as X-File-Id , so it knows which file we’re
uploading, and the starting byte as X-Start-Byte , so it knows we’re not
uploading it initially, but resuming.
The server should check its records, and if there was an upload of that file, and
the current uploaded size is exactly X-Start-Byte , then append the data to it.
Here’s the demo with both client and server code, written on Node.js.
It works only partially on this site, as Node.js is behind another server named Nginx,
that buffers uploads, passing them to Node.js when fully complete.
But you can download it and run locally for the full demonstration:
https://plnkr.co/edit/uwIHsRek1zB1NhjxDjC9?p=preview
As you can see, modern networking methods are close to file managers in their
capabilities – control over headers, progress indicator, sending file parts, etc.
Long polling
Long polling is the simplest way of having persistent connection with server, that
doesn’t use any specific protocol like WebSocket or Server Side Events.
Being very easy to implement, it’s also good enough in a lot of cases.
Regular Polling
The simplest way to get new information from the server is polling.
That is, periodical requests to the server: “Hello, I’m here, do you have any
information for me?”. For example, once in 10 seconds.
In response, the server first takes a notice to itself that the client is online, and
second – sends a packet of messages it got till that moment.
That works, but there are downsides:
1. Messages are passed with a delay up to 10 seconds.
2. Even if there are no messages, the server is bombed with requests every 10
seconds. That’s quite a load to handle for backend, speaking performance-wise.
So, if we’re talking about a very small service, the approach may be viable.
But generally, it needs an improvement.
Long polling
The situation when the browser sent a request and has a pending connection with
the server, is standard for this method. Only when a message is delivered, the
connection is reestablished.
Even if the connection is lost, because of, say, a network error, the browser
immediately sends a new request.
A sketch of client-side code:
if (response.status == 502) {
// Connection timeout, happens when the connection was pending for too long
// let's reconnect
await subscribe();
} else if (response.status != 200) {
// Show Error
showMessage(response.statusText);
// Reconnect in one second
await new Promise(resolve => setTimeout(resolve, 1000));
await subscribe();
} else {
// Got message
let message = await response.text();
showMessage(message);
await subscribe();
}
}
subscribe();
The subscribe() function makes a fetch, then waits for the response, handles it
and calls itself again.
Demo: a chat
Here’s a demo:
https://plnkr.co/edit/p0VXeg5MIwpxPjq7LWdR?p=preview
Area of usage
WebSocket
The WebSocket protocol, described in the specification RFC 6455 provides a
way to exchange data between browser and server via a persistent connection.
Once a websocket connection is established, both client and server may send the
data to each other.
WebSocket is especially great for services that require continuous data exchange,
e.g. online games, real-time trading systems and so on.
A simple example
There’s also encrypted wss:// protocol. It’s like HTTPS for websockets.
The wss:// protocol not only encrypted, but also more reliable.
That’s because ws:// data is not encrypted, visible for any intermediary. Old
proxy servers do not know about WebSocket, they may see “strange” headers
and abort the connection.
On the other hand, wss:// is WebSocket over TLS, (same as HTTPS is HTTP
over TLS), the transport security layer encrypts the data at sender and decrypts
at the receiver, so it passes encrypted through proxies. They can’t see what’s
inside and let it through.
Once the socket is created, we should listen to events on it. There are totally 4
events:
● open – connection established,
● message – data received,
● error – websocket error,
● close – connection closed.
Here’s an example:
socket.onopen = function(e) {
alert("[open] Connection established, send -> server");
socket.send("My name is John");
};
socket.onmessage = function(event) {
alert(`[message] Data received: ${event.data} <- server`);
};
socket.onclose = function(event) {
if (event.wasClean) {
alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reas
} else {
// e.g. server process killed or network down
// event.code is usually 1006 in this case
alert('[close] Connection died');
}
};
socket.onerror = function(error) {
alert(`[error] ${error.message}`);
};
For demo purposes, there’s a small server server.js written in Node.js, for the
example above, running. It responds with “hello”, then waits 5 seconds and closes
the connection.
So you’ll see events open → message → close .
That’s actually it, we can talk WebSocket already. Quite simple, isn’t it?
Now let’s talk more in-depth.
Opening a websocket
When new WebSocket(url) is created, it starts connecting immediately.
During the connection the browser (using headers) asks the server: “Do you support
Websocket?” And if the server replies “yes”, then the talk continues in WebSocket
protocol, which is not HTTP at all.
GET /chat
Host: javascript.info
Origin: https://javascript.info
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
●
Origin – the origin of the client page, e.g. https://javascript.info .
WebSocket objects are cross-origin by nature. There are no special headers or
other limitations. Old servers are unable to handle WebSocket anyway, so there
are no compabitility issues. But Origin header is important, as it allows the
server to decide whether or not to talk WebSocket with this website.
●
Connection: Upgrade – signals that the client would like to change the
protocol.
● Upgrade: websocket – the requested protocol is “websocket”.
● Sec-WebSocket-Key – a random browser-generated key for security.
● Sec-WebSocket-Version – WebSocket protocol version, 13 is the current
one.
WebSocket handshake can’t be emulated
We can’t use XMLHttpRequest or fetch to make this kind of HTTP-request,
because JavaScript is not allowed to set these headers.
If the server agrees to switch to WebSocket, it should send code 101 response:
For instance:
● Sec-WebSocket-Extensions: deflate-frame means that the browser
supports data compression. An extension is something related to transferring the
data, not data itself.
●
Sec-WebSocket-Protocol: soap, wamp means that we’d like to transfer
not just any data, but the data in SOAP or WAMP (“The WebSocket
Application Messaging Protocol”) protocols. WebSocket subprotocols are
registered in the IANA catalogue .
The server should respond with a list of protocols and extensions that it agrees to
use.
For example, the request:
GET /chat
Host: javascript.info
Upgrade: websocket
Connection: Upgrade
Origin: https://javascript.info
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap, wamp
Response:
Here the server responds that it supports the extension “deflate-frame”, and only
SOAP of the requested subprotocols.
WebSocket data
When we receive the data, text always comes as string. And for binary data, we
can choose between Blob and ArrayBuffer formats.
socket.bufferType = "arraybuffer";
socket.onmessage = (event) => {
// event.data is either a string (if text) or arraybuffer (if binary)
};
Rate limiting
Imagine, our app is generating a lot of data to send. But the user has a slow network
connection, maybe on a mobile, outside of a city.
We can call socket.send(data) again and again. But the data will be buffered
(stored) in memory and sent out only as fast as network speed allows.
The socket.bufferedAmount property stores how many bytes are buffered at
this moment, waiting to be sent over the network.
We can examine it to see whether the socket is actually available for transmission.
Connection close
Normally, when a party wants to close the connection (both browser and server have
equal rights), they send a “connection close frame” with a numeric code and a
textual reason.
The method for that is:
socket.close([code], [reason]);
●
code is a special WebSocket closing code (optional)
●
reason is a string that describes the reason of closing (optional)
Then the other party in close event handler gets the code and the reason, e.g.:
// closing party:
socket.close(1000, "Work complete");
WebSocket codes are somewhat like HTTP codes, but different. In particular, any
codes less than 1000 are reserved, there’ll be an error if we try to set such a code.
Connection state
Chat example
Let’s review a chat example using browser WebSocket API and Node.js WebSocket
module https://github.com/websockets/ws .
HTML: there’s a <form> to send messages and a <div> for incoming messages:
socket.send(outgoingMessage);
return false;
};
Server-side code is a little bit beyond our scope here. We’re using browser
WebSocket API, a server may have another library.
Still it can also be pretty simple. We’ll use Node.js with
https://github.com/websockets/ws module for websockets.
The server-side algorithm will be:
1. Create clients = new Set() – a set of sockets.
2. For each accepted websocket, clients.add(socket) and add message
event listener for its messages.
3. When a message received: iterate over clients and send it to everyone.
4. When a connection is closed: clients.delete(socket) .
function onSocketConnect(ws) {
clients.add(ws);
ws.on('message', function(message) {
message = message.slice(0, 50); // max message length will be 50
ws.on('close', function() {
clients.delete(ws);
});
}
Send
You can also download it (upper-right button in the iframe) and run locally. Just don’t
forget to install Node.js and npm install ws before running.
Summary
WebSocket is a modern way to have persistent browser-server connections.
● WebSockets don’t have cross-origin limitations.
●
They are well-supported in browsers.
●
Can send/receive strings and binary data.
Events:
●
open ,
● message ,
●
error ,
●
close .
WebSocket by itself does not include reconnection, authentication and many other
high-level mechanisms. So there are client/server libraries for that, and it’s also
possible to implement these capabilities manually.
Sometimes, to integrate WebSocket into existing project, people run WebSocket
server in parallel with the main HTTP-server, and they share a single database.
Requests to WebSocket use wss://ws.site.com , a subdomain that leads to
WebSocket server, while https://site.com goes to the main HTTP-server.
Surely, other ways of integration are also possible. Many servers (such as Node.js)
can support both HTTP and WebSocket protocols.
WebSocket EventSource
Bi-directional: both client and server can exchange messages One-directional: only server sends data
Getting messages
The browser will connect to url and keep the connection open, waiting for events.
The server should respond with status 200 and the header Content-Type:
text/event-stream , then keep the connection and write messages into it in the
special format, like this:
data: Message 1
data: Message 2
data: Message 3
data: of two lines
●
A message text goes after data: , the space after the semicolon is optional.
●
Messages are delimited with double line breaks \n\n .
●
To send a line break \n , we can immediately one more data: (3rd message
above).
…So we can assume that one data: holds exactly one message.
eventSource.onmessage = function(event) {
console.log("New message", event.data);
// will log 3 times for the data stream above
};
// or eventSource.addEventListener('message', ...)
Cross-domain requests
EventSource supports cross-origin requests, like fetch any other networking
methods. We can use any URL:
The remote server will get the Origin header and must respond with Access-
Control-Allow-Origin to proceed.
Please see the chapter Fetch: Cross-Origin Requests for more details about cross-
domain headers.
Reconnection
Upon creation, new EventSource connects to the server, and if the connection is
broken – reconnects.
That’s very convenient, as we don’t have to care about it.
There’s a small delay between reconnections, a few seconds by default.
The server can set the recommended delay using retry: in response (in
milliseconds):
retry: 15000
data: Hello, I set the reconnection delay to 15 seconds
The retry: may come both together with some data, or as a standalone message.
The browser should wait that much before reconnect. If the network connection is
lost, the browser may wait till it’s restored, and then retry.
●
If the server wants the browser to stop reconnecting, it should respond with HTTP
status 204.
●
If the browser wants to close the connection, it should call
eventSource.close() :
eventSource.close();
Please note:
There’s no way to “reopen” a closed connection. If we’d like to connect again,
just create a new EventSource .
Message id
When a connection breaks due to network problems, either side can’t be sure which
messages were received, and which weren’t.
To correctly resume the connection, each message should have an id field, like
this:
data: Message 1
id: 1
data: Message 2
id: 2
data: Message 3
data: of two lines
id: 3
Please note: the id: is appended below the message data, to ensure that
lastEventId is updated after the message data is received.
The EventSource object has readyState property, that has one of three
values:
Event types
The server may specify another type of event with event: ... at the event start.
For example:
event: join
data: Bob
data: Hello
event: leave
data: Bob
To handle custom events, we must use addEventListener , not onmessage :
Full example
Here’s the server that sends messages with 1 , 2 , 3 , then bye and breaks the
connection.
Then the browser automatically reconnects.
https://plnkr.co/edit/LmOdPFHJdD3yIrRGhkSF?p=preview
Summary
Overall cross-domain security is same as for fetch and other network methods.
readyState
The current connection state: either EventSource.CONNECTING (=0) ,
EventSource.OPEN (=1) or EventSource.CLOSED (=2) .
lastEventId
The last received id . Upon reconnection the browser sends it in the header Last-
Event-ID .
Methods
close()
Closes the connection соединение.
Events
message
Message received, the data is in event.data .
open
The connection is established.
error
In case of an error, including both lost connection (will auto-reconnect) and fatal
errors. We can check readyState to see if the reconnection is being attempted.
The server may set a custom event name in event: . Such events should be
handled using addEventListener , not on<event> .
We can also access cookies from the browser, using document.cookie property.
There are many tricky things about cookies and their options. In this chapter we’ll
cover them in detail.
Assuming you’re on a website, it’s possible to see the cookies from it, like this:
Writing to document.cookie
We can write to document.cookie . But it’s not a data property, it’s an accessor.
An assignment to it is treated specially.
A write operation to document.cookie passes through the browser that
updates cookies mentioned in it, but doesn’t touch other cookies.
For instance, this call sets a cookie with the name user and value John :
If you run it, then probably you’ll see multiple cookies. That’s because
document.cookie= operation does not overwrite all cookies. It only sets the
mentioned cookie user .
Technically, name and value can have any characters, but to keep the formatting
valid they should be escaped using a built-in encodeURIComponent function:
⚠ Limitations
There are few limitations:
● The name=value pair, after encodeURIComponent , should not exceed
4kb. So we can’t store anything huge in a cookie.
●
The total number of cookies per domain is limited to around 20+, the exact
limit depends on a browser.
Cookies have several options, many of them are important and should be set.
The options are listed after key=value , delimited by ; , like this:
path
● path=/mypath
The url path prefix, the cookie will be accessible for pages under that path. Must be
absolute. By default, it’s the current path.
If a cookie is set with path=/admin , it’s visible at pages /admin and
/admin/something , but not at /home or /adminpage .
Usually, we should set path to the root: path=/ to make the cookie accessible
from all website pages.
domain
●
domain=site.com
A domain where the cookie is accessible. In practice though, there are limitations.
We can’t set any domain.
By default, a cookie is accessible only at the domain that set it. So, if the cookie was
set by site.com , we won’t get it other.com .
…But what’s more tricky, we also won’t get the cookie at a subdomain
forum.site.com !
// at site.com
document.cookie = "user=John"
// at forum.site.com
alert(document.cookie); // no user
It’s a safety restriction, to allow us to store sensitive data in cookies, that should be
available only on one site.
…But if we’d like to allow subdomains like forum.site.com get a cookie, that’s
possible. When setting a cookie at site.com , we should explicitly set domain
option to the root domain: domain=site.com :
// at site.com
// make the cookie accessible on any subdomain *.site.com:
document.cookie = "user=John; domain=site.com"
// later
// at forum.site.com
alert(document.cookie); // has cookie user=John
expires, max-age
By default, if a cookie doesn’t have one of these options, it disappears when the
browser is closed. Such cookies are called “session cookies”
To let cookies survive browser close, we can set either expires or max-age
option.
●
expires=Tue, 19 Jan 2038 03:14:07 GMT
That is, cookies are domain-based, they do not distinguish between the protocols.
With this option, if a cookie is set by https://site.com , then it doesn’t appear
when the same site is accessed by HTTP, as http://site.com . So if a cookie
has sensitive content that should never be sent over unencrypted HTTP, then the
flag is the right thing.
samesite
That’s another security attribute somesite . It’s designed to protect from so-called
XSRF (cross-site request forgery) attacks.
To understand how it works and when it’s useful, let’s take a look at XSRF attacks.
XSRF attack
Imagine, you are logged into the site bank.com . That is: you have an
authentication cookie from that site. Your browser sends it to bank.com with every
request, so that it recognizes you and performs all sensitive financial operations.
Now, while browsing the web in another window, you occasionally come to another
site evil.com , that automatically submits a form <form
action="https://bank.com/pay"> to bank.com with input fields that initiate
a transaction to the hacker’s account.
The form is submitted from evil.com directly to the bank site, and your cookie is
also sent, just because it’s sent every time you visit bank.com . So the bank
recognizes you and actually performs the payment.
That’s called a cross-site request forgery (or XSRF) attack.
Real banks are protected from it of course. All forms generated by bank.com have
a special field, so called “xsrf protection token”, that an evil page can’t neither
generate, nor somehow extract from a remote page (it can submit a form there, but
can’t get the data back).
But that takes time to implement: we need to ensure that every form has the token
field, and we must also check all requests.
A cookie with samesite=strict is never sent if the user comes from outside the
site.
In other words, whether a user follows a link from their mail or submits a form from
evil.com , or does any operation that originates from another domain, the cookie
is not sent.
If authentication cookies have samesite option, then XSRF attack has no chances
to succeed, because a submission from evil.com comes without cookies. So
bank.com will not recognize the user and will not proceed with the payment.
The protection is quite reliable. Only operations that come from bank.com will send
the samesite cookie.
We could work around that by using two cookies: one for “general recognition”, only
for the purposes of saying: “Hello, John”, and the other one for data-changing
operations with samesite=strict . Then a person coming from outside of the site
will see a welcome, but payments must be initiated from the bank website.
●
samesite=lax
A more relaxed approach that also protects from XSRF and doesn’t break user
experience.
Lax mode, just like strict , forbids the browser to send cookies when coming from
outside the site, but adds an exception.
A samesite=lax cookie is sent if both of these conditions are true:
So, what samesite=lax does is basically allows a most common “go to URL”
operation to have cookies. E.g. opening a website link from notes satisfies these
conditions.
But anything more complicated, like AJAX request from another site or a form
submittion loses cookies.
If that’s fine for you, then adding samesite=lax will probably not break the user
experience and add protection.
Overall, samesite is a great option, but it has an important drawback:
●
samesite is ignored (not supported) by old browsers, year 2017 or so.
httpOnly
This option has nothing to do with JavaScript, but we have to mention it for
completeness.
The web-server uses Set-Cookie header to set a cookie. And it may set the
httpOnly option.
This option forbids any JavaScript access to the cookie. We can’t see such cookie or
manipulate it using document.cookie .
That’s used as a precaution measure, to protect from certain attacks when a hacker
injects his own JavaScript code into a page and waits for a user to visit that page.
That shouldn’t be possible at all, a hacker should not be able to inject their code into
our site, but there may be bugs that let hackers do it.
Normally, if such thing happens, and a user visits a web-page with hacker’s code,
then that code executes and gains access to document.cookie with user cookies
containing authentication information. That’s bad.
But if a cookie is httpOnly , then document.cookie doesn’t see it, so it is
protected.
Here’s a small set of functions to work with cookies, more convenient than a manual
modification of document.cookie .
There exist many cookie libraries for that, so these are for demo purposes. Fully
working though.
getCookie(name)
The shortest way to access cookie is to use a regular expression.
The function getCookie(name) returns the cookie with the given name :
options = {
path: '/',
// add other defaults here if necessary
...options
};
if (options.expires.toUTCString) {
options.expires = options.expires.toUTCString();
}
document.cookie = updatedCookie;
}
// Example of use:
setCookie('user', 'John', {secure: true, 'max-age': 3600});
deleteCookie(name)
To delete a cookie, we can call it with a negative expiration date:
function deleteCookie(name) {
setCookie(name, "", {
'max-age': -1
})
}
Together: cookie.js.
A cookie is called “third-party” if it’s placed by domain other than the user is visiting.
For instance:
1. A page at site.com loads a banner from another site: <img
src="https://ads.com/banner.png"> .
2. Along with the banner, the remote server at ads.com may set Set-Cookie
header with cookie like id=1234 . Such cookie originates from ads.com
domain, and will only be visible at ads.com :
3. Next time when ads.com is accessed, the remote server gets the id cookie
and recognizes the user:
4. What’s even more important, when the users moves from site.com to another
site other.com that also has a banner, then ads.com gets the cookie, as it
belongs to ads.com , thus recognizing the visitor and tracking him as he moves
between sites:
Third-party cookies are traditionally used for tracking and ads services, due to their
nature. They are bound to the originating domain, so ads.com can track the same
user between different sites, if they all access it.
Naturally, some people don’t like being tracked, so browsers allow to disable such
cookies.
Also, some modern browsers employ special policies for such cookies:
●
Safari does not allow third-party cookies at all.
● Firefox comes with a “black list” of third-party domains where it blocks third-party
cookies.
Please note:
If we load a script from a third-party domain, like <script
src="https://google-analytics.com/analytics.js"> , and that
script uses document.cookie to set a cookie, then such cookie is not third-
party.
If a script sets a cookie, then no matter where the script came from – it belongs
to the domain of the current webpage.
Appendix: GDPR
This topic is not related to JavaScript at all, just something to keep in mind when
setting cookies.
There’s a legislation in Europe called GDPR, that enforces a set of rules for websites
to respect users’ privacy. And one of such rules is to require an explicit permission
for tracking cookies from a user.
Please note, that’s only about tracking/identifying cookies.
So, if we set a cookie that just saves some information, but neither tracks nor
identifies the user, then we are free to do it.
But if we are going to set a cookie with an authentication session or a tracking id,
then a user must allow that.
Websites generally have two variants of following GDPR. You must have seen them
both already in the web:
1. If a website wants to set tracking cookies only for authenticated users.
To do so, the registration form should have a checkbox like “accept the privacy
policy”, the user must check it, and then the website is free to set auth cookies.
2. If a website wants to set tracking cookies for everyone.
To do so legally, a website shows a modal “splash screen” for newcomers, and
require them to agree for cookies. Then the website can set them and let people
see the content. That can be disturbing for new visitors though. No one likes to
see “must-click” modal splash screens instead of the content. But GDPR requires
an explicit agreement.
GDPR is not only about cookies, it’s about other privacy-related issues too, but that’s
too much beyond our scope.
Summary
Cookie options:
●
path=/ , by default current path, makes the cookie visible only under that path.
●
domain=site.com , by default a cookie is visible on current domain only, if set
explicitly to the domain, makes the cookie visible on subdomains.
●
expires or max-age sets cookie expiration time, without them the cookie dies
when the browser is closed.
●
secure makes the cookie HTTPS-only.
●
samesite forbids the browser to send the cookie with requests coming from
outside the site, helps to prevent XSRF attacks.
Additionally:
● Third-party cookies may be forbidden by the browser, e.g. Safari does that by
default.
●
When setting a tracking cookie for EU citizens, GDPR requires to ask for
permission.
LocalStorage, sessionStorage
Web storage objects localStorage and sessionStorage allow to save
key/value pairs in the browser.
What’s interesting about them is that the data survives a page refresh (for
sessionStorage ) and even a full browser restart (for localStorage ). We’ll
see that very soon.
We already have cookies. Why additional objects?
● Unlike cookies, web storage objects are not sent to server with each request.
Because of that, we can store much more. Most browsers allow at least 2
megabytes of data (or more) and have settings to configure that.
●
Also unlike cookies, the server can’t manipulate storage objects via HTTP
headers. Everything’s done in JavaScript.
● The storage is bound to the origin (domain/protocol/port triplet). That is, different
protocols or subdomains infer different storage objects, they can’t access data
from each other.
localStorage demo
localStorage.setItem('test', 1);
…And close/open the browser or just open the same page in a different window,
then you can get it like this:
alert( localStorage.getItem('test') ); // 1
We only have to be on the same origin (domain/port/protocol), the url path can be
different.
The localStorage is shared between all windows with the same origin, so if we
set the data in one window, the change becomes visible in another one.
Object-like access
We can also use a plain object way of getting/setting keys, like this:
// set key
localStorage.test = 2;
// get key
alert( localStorage.test ); // 2
// remove key
delete localStorage.test;
That’s allowed for historical reasons, and mostly works, but generally not
recommended for two reasons:
1. If the key is user-generated, it can be anything, like length or toString , or
another built-in method of localStorage . In that case getItem/setItem
work fine, while object-like access fails:
2. There’s a storage event, it triggers when we modify the data. That event does
not happen for object-like access. We’ll see that later in this chapter.
As we’ve seen, the methods provide “get/set/remove by key” functionality. But how to
get all saved values or keys?
Unfortunately, storage objects are not iterable.
One way is to loop over them as over an array:
// bad try
for(let key in localStorage) {
alert(key); // shows getItem, setItem and other built-in stuff
}
…So we need either to filter fields from the prototype with hasOwnProperty
check:
for(let key in localStorage) {
if (!localStorage.hasOwnProperty(key)) {
continue; // skip keys like "setItem", "getItem" etc
}
alert(`${key}: ${localStorage.getItem(key)}`);
}
…Or just get the “own” keys with Object.keys and then loop over them if
needed:
The latter works, because Object.keys only returns the keys that belong to the
object, ignoring the prototype.
Strings only
// sometime later
let user = JSON.parse( sessionStorage.user );
alert( user.name ); // John
Also it is possible to stringify the whole storage object, e.g. for debugging purposes:
Properties and methods are the same, but it’s much more limited:
●
The sessionStorage exists only within the current browser tab.
● Another tab with the same page will have a different storage.
● But it is shared between iframes in the tab (assuming they come from the same
origin).
● The data survives page refresh, but not closing/opening the tab.
sessionStorage.setItem('test', 1);
…Then refresh the page. Now you can still get the data:
…But if you open the same page in another tab, and try again there, the code above
returns null , meaning “nothing found”.
That’s exactly because sessionStorage is bound not only to the origin, but also
to the browser tab. For that reason, sessionStorage is used sparingly.
Storage event
The important thing is: the event triggers on all window objects where the storage
is accessible, except the one that caused it.
Let’s elaborate.
Imagine, you have two windows with the same site in each. So localStorage is
shared between them.
If both windows are listening for window.onstorage , then each one will react on
updates that happened in the other one.
localStorage.setItem('now', Date.now());
Please note that the event also contains: event.url – the url of the document
where the data was updated.
Also, event.storageArea contains the storage object – the event is the same
for both sessionStorage and localStorage , so storageArea references
the one that was modified. We may even want to set something back in it, to
“respond” to a change.
That allows different windows from the same origin to exchange messages.
Modern browsers also support Broadcast channel API , the special API for same-
origin inter-window communication, it’s more full featured, but less supported. There
are libraries that polyfill that API, based on localStorage , that make it available
everywhere.
Summary
localStorage sessionStorage
Shared between all tabs and windows with the Visible within a browser tab, including iframes from the
same origin same origin
Survives browser restart Survives page refresh (but not tab close)
API:
●
setItem(key, value) – store key/value pair.
● getItem(key) – get the value by key.
●
removeItem(key) – remove the key with its value.
● clear() – delete everything.
●
key(index) – get the key number index .
● length – the number of stored items.
● Use Object.keys to get all keys.
● We access keys as object properties, in that case storage event isn’t triggered.
Storage event:
● Triggers on setItem , removeItem , clear calls.
●
Contains all the data about the operation, the document url and the storage
object.
●
Triggers on all window objects that have access to the storage except the one
that generated it (within a tab for sessionStorage , globally for
localStorage ).
✔ Tasks
So, if the user occasionally closes the page, and opens it again, he’ll find his
unfinished input at place.
Like this:
Write here
Clear
To solution
IndexedDB
IndexedDB is a built-in database, much more powerful than localStorage .
●
Key/value storage: value can be (almost) anything, multiple key types.
● Supports transactions for reliability.
●
Supports key range queries, indexes.
● Can store much more data than localStorage .
We can also use async/await with the help of a promise-based wrapper, like
https://github.com/jakearchibald/idb . That’s pretty convenient, but the wrapper is
not perfect, it can’t replace events for all cases. So we’ll start with events, and then,
after we gain understanding of IndexedDb, we’ll use the wrapper.
Open database
●
name – a string, the database name.
●
version – a positive integer version, by default 1 (explained below).
We can have many databases with different names, but all of them exist within the
current origin (domain/protocol/port). Different websites can’t access databases of
each other.
After the call, we need to listen to events on openRequest object:
● success : database is ready, there’s the “database object” in
openRequest.result , that we should use it for further calls.
● error : opening failed.
●
upgradeneeded : database is ready, but its version is outdated (see below).
IndexedDB has a built-in mechanism of “schema versioning”, absent in server-
side databases.
Unlike server-side databases, IndexedDB is client-side, the data is stored in the
browser, so we, developers, don’t have direct access to it. But when we publish a
new version of our app, we may need to update the database.
If the local database version is less than specified in open , then a special event
upgradeneeded is triggered, and we can compare versions and upgrade data
structures as needed.
The event also triggers when the database did not exist yet, so we can perform
initialization.
When we first publish our app, we open it with version 1 and perform the
initialization in upgradeneeded handler:
openRequest.onupgradeneeded = function() {
// triggers if the client had no database
// ...perform initialization...
};
openRequest.onerror = function() {
console.error("Error", openRequest.error);
};
openRequest.onsuccess = function() {
let db = openRequest.result;
// continue to work with database using db object
};
openRequest.onupgradeneeded = function() {
// the existing database version is less than 2 (or it doesn't exist)
let db = openRequest.result;
switch(db.version) { // existing db version
case 0:
// version 0 means that the client had no database
// perform initialization
case 1:
// client had version 1
// update
}
};
So, in openRequest.onupgradeneeded we update the database. Soon we’ll
see how it’s done. And then, only if its handler finishes without errors,
openRequest.onsuccess triggers.
To delete a database:
Such thing may happen if the visitor loaded an outdated code, e.g. from a proxy
cache. We should check db.version , suggest him to reload the page. And
also re-check our caching headers to ensure that the visitor never gets old code.
The problem is that a database is shared between two tabs, as that’s the same site,
same origin. And it can’t be both version 1 and 2. To perform the update to version 2,
all connections to version 1 must be closed.
In order to organize that, the versionchange event triggers an open database
object when a parallel upgrade is attempted. We should listen to it, so that we should
close the database (and probably suggest the visitor to reload the page, to load the
updated code).
If we don’t close it, then the second, new connection will be blocked with blocked
event instead of success .
openRequest.onupgradeneeded = ...;
openRequest.onerror = ...;
openRequest.onsuccess = function() {
let db = openRequest.result;
db.onversionchange = function() {
db.close();
alert("Database is outdated, please reload the page.")
};
openRequest.onblocked = function() {
// there's another open connection to same database
// and it wasn't closed after db.onversionchange triggered for them
};
There are other variants. For example, we can take time to close things gracefully in
db.onversionchange , prompt the visitor to save the data before the connection
is closed. The new updating connection will be blocked immediatelly after
db.onversionchange finished without closing, and we can ask the visitor in the
new tab to close other tabs for the update.
Such update collision happens rarely, but we should at least have some handling for
it, e.g. onblocked handler, so that our script doesn’t surprise the user by dying
silently.
Object store
A key must have a type one of: number, date, string, binary, or array. It’s an unique
identifier: we can search/remove/update values by the key.
As we’ll see very soon, we can provide a key when we add a value to the store,
similar to localStorage . But when we store objects, IndexedDB allows to setup
an object property as the key, that’s much more convenient. Or we can auto-
generate keys.
But we need to create an object store first.
The syntax to create an object store:
db.createObjectStore(name[, keyOptions]);
db.deleteObjectStore('books')
Transactions
The term “transaction” is generic, used in many kinds of databases.
A transaction is a group operations, that should either all succeed or all fail.
For instance, when a person buys something, we need:
1. Subtract the money from their account.
2. Add the item to their inventory.
It would be pretty bad if we complete the 1st operation, and then something goes
wrong, e.g. lights out, and we fail to do the 2nd. Both should either succeed
(purchase complete, good!) or both fail (at least the person kept their money, so they
can retry).
Transactions can guarantee that.
db.transaction(store[, type]);
● store is a store name that the transaction is going to access, e.g. "books" .
Can be an array of store names if we’re going to access multiple stores.
● type – a transaction type, one of:
●
readonly – can only read, the default.
● readwrite – can only read and write the data, but not create/remove/alter
object stores.
Many readonly transactions are able to access concurrently the same store,
but readwrite transactions can’t. A readwrite transaction “locks” the store
for writing. The next transaction must wait before the previous one finishes
before accessing the same store.
After the transaction is created, we can add an item to the store, like this:
let transaction = db.transaction("books", "readwrite"); // (1)
let book = {
id: 'js',
price: 10,
created: new Date()
};
request.onerror = function() {
console.log("Error", request.error);
};
Transactions’ autocommit
In the example above we started the transaction and made add request. But as we
stated previously, a transaction may have multiple associated requests, that must
either all success or all fail. How do we mark the transaction as finished, no more
requests to come?
The short answer is: we don’t.
In the next version 3.0 of the specification, there will probably be a manual way to
finish the transaction, but right now in 2.0 there isn’t.
When all transaction requests are finished, and the microtasks queue is empty,
it is committed automatically.
Usually, we can assume that a transaction commits when all its requests are
complete, and the current code finishes.
So, in the example above no special call is needed to finish the transaction.
Transactions auto-commit principle has an important side effect. We can’t insert an
async operation like fetch , setTimeout in the middle of transaction. IndexedDB
will not keep the transaction waiting till these are done.
In the code below request2 in line (*) fails, because the transaction is already
committed, can’t make any request in it:
request1.onsuccess = function() {
fetch('/').then(response => {
let request2 = books.add(anotherBook); // (*)
request2.onerror = function() {
console.log(request2.error.name); // TransactionInactiveError
};
});
};
But it will be even better, if we’d like to keep the operations together, in one
transaction, to split apart IndexedDB transactions and “other” async stuff.
First, make fetch , prepare the data if needed, afterwards create a transaction and
perform all the database requests, it’ll work then.
To detect the moment of successful completion, we can listen to
transaction.oncomplete event:
// ...perform operations...
transaction.oncomplete = function() {
console.log("Transaction is complete");
};
transaction.abort();
Error handling
request.onerror = function(event) {
// ConstraintError occurs when an object with the same id already exists
if (request.error.name == "ConstraintError") {
console.log("Book with such id already exists"); // handle the error
event.preventDefault(); // don't abort the transaction
// use another key for the book?
} else {
// unexpected error, can't handle it
// the transaction will abort
}
};
transaction.onabort = function() {
console.log("Error", transaction.error);
};
Event delegation
Do we need onerror/onsuccess for every request? Not every time. We can use event
delegation instead.
IndexedDB events bubble: request → transaction → database .
All events are DOM events, with capturing and bubbling, but usually only bubbling
stage is used.
So we can catch all errors using db.onerror handler, for reporting or other
purposes:
db.onerror = function(event) {
let request = event.target; // the request that caused the error
console.log("Error", request.error);
};
…But what if an error is fully handled? We don’t want to report it in that case.
We can stop the bubbling and hence db.onerror by using
event.stopPropagation() in request.onerror .
request.onerror = function(event) {
if (request.error.name == "ConstraintError") {
console.log("Book with such id already exists"); // handle the error
event.preventDefault(); // don't abort the transaction
event.stopPropagation(); // don't bubble error up, "chew" it
} else {
// do nothing
// transaction will be aborted
// we can take care of error in transaction.onabort
}
};
Searching by keys
First let’s deal with the keys and key ranges (1) .
Methods that involve searching support either exact keys or so-called “range
queries” – IDBKeyRange objects that specify a “key range”.
All searching methods accept a query argument that can be either an exact key or
a key range:
●
store.get(query) – search for the first value by a key or a range.
●
store.getAll([query], [count]) – search for all values, limit by count
if given.
● store.getKey(query) – search for the first key that satisfies the query,
usually a range.
●
store.getAllKeys([query], [count]) – search for all keys that satisfy
the query, usually a range, up to count if given.
●
store.count([query]) – get the total count of keys that satisfy the query,
usually a range.
For instance, we have a lot of books in our store. Remember, the id field is the key,
so all these methods can search by id .
Request examples:
●
name – index name,
●
keyPath – path to the object field that the index should track (we’re going to
search by that field),
● option – an optional object with properties:
● unique – if true, then there may be only one object in the store with the given
value at the keyPath . The index will enforce that by generating an error if we
try to add a duplicate.
●
multiEntry – only used if the value on keyPath is an array. In that case,
by default, the index will treat the whole array as the key. But if multiEntry
is true, then the index will keep a list of store objects for each value in that
array. So array members become index keys.
openRequest.onupgradeneeded = function() {
// we must create the index here, in versionchange transaction
let books = db.createObjectStore('books', {keyPath: 'id'});
let index = inventory.createIndex('price_idx', 'price');
};
Imagine that our inventory has 4 books. Here’s the picture that shows exactly
what the index is:
As said, the index for each value of price (second argument) keeps the list of keys
that have that price.
The index keeps itself up to date automatically, we don’t have to care about it.
Now, when we want to search for a given price, we simply apply the same search
methods to the index:
request.onsuccess = function() {
if (request.result !== undefined) {
console.log("Books", request.result); // array of books with price=10
} else {
console.log("No such books");
}
};
We can also use IDBKeyRange to create ranges and looks for cheap/expensive
books:
Indexes are internally sorted by the tracked object field, price in our case. So
when we do the search, the results are also sorted by price .
The delete method looks up values to delete by a query, the call format is similar
to getAll :
● delete(query) – delete matching values by query.
For instance:
request.onsuccess = function() {
let id = request.result;
let deleteRequest = books.delete(id);
};
To delete everything:
Cursors
But an object storage can be huge, bigger than the available memory. Then
getAll will fail to get all records as an array.
What to do?
Cursors provide the means to work around that.
A cursor is a special object that traverses the object storage, given a query,
and returns one key/value at a time, thus saving memory.
As an object store is sorted internally by key, a cursor walks the store in key order
(ascending by default).
The syntax:
●
query is a key or a key range, same as for getAll .
● direction is an optional argument, which order to use:
●
"next" – the default, the cursor walks up from the record with the lowest key.
● "prev" – the reverse order: down from the record with the biggest key.
● "nextunique" , "prevunique" – same as above, but skip records with
the same key (only for cursors over indexes, e.g. for multiple books with
price=5 only the first one will be returned).
Whether there are more values matching the cursor or not – onsuccess gets
called, and then in result we can get the cursor pointing to the next record, or
undefined .
In the example above the cursor was made for the object store.
But we also can make a cursor over an index. As we remember, indexes allow to
search by an object field. Cursors over indexes to precisely the same as over object
stores – they save memory by returning one value at a time.
For cursors over indexes, cursor.key is the index key (e.g. price), and we should
use cursor.primaryKey property the object key:
Promise wrapper
try {
await books.add(...);
await books.add(...);
await transaction.complete;
console.log('jsbook saved');
} catch(err) {
console.log('error', err.message);
}
So we have all the sweet “plain async code” and “try…catch” stuff.
Error handling
If we don’t catch an error, then it falls through, till the closest outer try..catch .
await inventory.add({ id: 'js', price: 10, created: new Date() });
await inventory.add({ id: 'js', price: 10, created: new Date() }); // Error
The next inventory.add after fetch (*) fails with an “inactive transaction”
error, because the transaction is already committed and closed at that time.
The workaround is same as when working with native IndexedDB: either make a
new transaction or just split things apart.
1. Prepare the data and fetch all that’s needed first.
2. Then save in the database.
In few rare cases, when we need the original request object, we can access it as
promise.request property of the promise:
let promise = books.add(book); // get a promise (don't await for its result)
Summary
Animation
CSS and JavaScript animations.
Bezier curve
Bezier curves are used in computer graphics to draw shapes, for CSS animation and
in many other places.
They are a very simple thing, worth to study once and then feel comfortable in the
world of vector graphics and advanced animations.
Control points
3 4
1 2
As you can notice, the curve stretches along the tangential lines 1 → 2 and 3
→ 4.
After some practice it becomes obvious how to place points to get the needed curve.
And by connecting several curves we can get practically anything.
Here are some examples:
De Casteljau’s algorithm
There’s a mathematical formula for Bezier curves, but let’s cover it a bit later,
because De Casteljau’s algorithm it is identical to the mathematical definition and
visually shows how it is constructed.
First let’s see the 3-points example.
Here’s the demo, and the explanation follow.
Control points (1,2 and 3) can be moved by the mouse. Press the “play” button to run
it.
t:1
2
1 3
2. Build segments between control points 1 → 2 → 3. In the demo above they are
brown.
3. The parameter t moves from 0 to 1 . In the example above the step 0.05 is
used: the loop goes over 0, 0.05, 0.1, 0.15, ... 0.95, 1 .
For each of these values of t :
● On each brown segment we take a point located on the distance proportional to
t from its beginning. As there are two segments, we have two points.
For instance, for t=0 – both points will be at the beginning of segments, and
for t=0.25 – on the 25% of segment length from the beginning, for t=0.5 –
50%(the middle), for t=1 – in the end of segments.
● Connect the points. On the picture below the connecting segment is painted
blue.
4. Now in the blue segment take a point on the distance proportional to the same
value of t . That is, for t=0.25 (the left picture) we have a point at the end of
the left quarter of the segment, and for t=0.5 (the right picture) – in the middle
of the segment. On pictures above that point is red.
5. As t runs from 0 to 1 , every value of t adds a point to the curve. The set of
such points forms the Bezier curve. It’s red and parabolic on the pictures above.
That was a process for 3 points. But the same is for 4 points.
The demo for 4 points (points can be moved by a mouse):
t:1
3 4
1 2
The algorithm is recursive and can be generalized for any number of control points.
Given N of control points:
1. We connect them to get initially N-1 segments.
2. Then for each t from 0 to 1 , we take a point on each segment on the distance
proportional to t and connect them. There will be N-2 segments.
3. Repeat step 2 until there is only one point.
t:1
4
3 2
1 4
t:1
3 2
1 4
As the algorithm is recursive, we can build Bezier curves of any order, that is: using
5, 6 or more control points. But in practice many points are less useful. Usually we
take 2-3 points, and for complex lines glue several curves together. That’s simpler to
develop and calculate.
How to draw a curve through given points?
To specify a Bezier curve, control points are used. As we can see, they are not
on the curve, except the first and the last ones.
Sometimes we have another task: to draw a curve through several points, so that
all of them are on a single smooth curve. That task is called interpolation , and
here we don’t cover it.
There are mathematical formulas for such curves, for instance Lagrange
polynomial . In computer graphics spline interpolation is often used to build
smooth curves that connect many points.
Maths
These are vector equations. In other words, we can put x and y instead of P to
get corresponding coordinates.
For instance, the 3-point curve is formed by points (x,y) calculated as:
●
x = (1−t)2x1 + 2(1−t)tx2 + t2x3
●
y = (1−t)2y1 + 2(1−t)ty2 + t2y3
Instead of x1, y1, x2, y2, x3, y3 we should put coordinates of 3 control
points, and then as t moves from 0 to 1 , for each value of t we’ll have (x,y)
of the curve.
For instance, if control points are (0,0) , (0.5, 1) and (1, 0) , the equations
become:
●
x = (1−t)2 * 0 + 2(1−t)t * 0.5 + t2 * 1 = (1-t)t + t2 = t
●
y = (1−t)2 * 0 + 2(1−t)t * 1 + t2 * 0 = 2(1-t)t = –t2 + 2t
Now as t runs from 0 to 1 , the set of values (x,y) for each t forms the curve
for such control points.
Summary
Usage:
●
In computer graphics, modeling, vector graphic editors. Fonts are described by
Bezier curves.
● In web development – for graphics on Canvas and in the SVG format. By the way,
“live” examples above are written in SVG. They are actually a single SVG
document that is given different points as parameters. You can open it in a
separate window and see the source: demo.svg.
● In CSS animation to describe the path and speed of animation.
CSS-animations
CSS animations allow to do simple animations without JavaScript at all.
JavaScript can be used to control CSS animation and make it even better with a little
of code.
CSS transitions
The idea of CSS transitions is simple. We describe a property and how its changes
should be animated. When the property changes, the browser paints the animation.
That is: all we need is to change the property. And the fluent transition is made by
the browser.
For instance, the CSS below animates changes of background-color for 3
seconds:
.animated {
transition-property: background-color;
transition-duration: 3s;
}
<style>
#color {
transition-property: background-color;
transition-duration: 3s;
}
</style>
<script>
color.onclick = function() {
this.style.backgroundColor = 'red';
};
</script>
Click me
We’ll cover them in a moment, for now let’s note that the common transition
property allows to declare them together in the order: property duration
timing-function delay , and also animate multiple properties at once.
<style>
#growing {
transition: font-size 3s, color 2s;
}
</style>
<script>
growing.onclick = function() {
this.style.fontSize = '36px';
this.style.color = 'red';
};
</script>
Click me
transition-property
Not all properties can be animated, but many of them . The value all means
“animate all properties”.
transition-duration
transition-delay
https://plnkr.co/edit/tRHA6fkSPUe9cjk35zPL?p=preview
#stripe.animate {
transform: translate(-90%);
transition-property: transform;
transition-duration: 9s;
}
In the example above JavaScript adds the class .animate to the element – and
the animation starts:
stripe.classList.add('animate');
We can also start it “from the middle”, from the exact number, e.g. corresponding to
the current second, using the negative transition-delay .
Here if you click the digit – it starts the animation from the current second:
https://plnkr.co/edit/zpqja4CejOmTApUXPxwE?p=preview
stripe.onclick = function() {
let sec = new Date().getSeconds() % 10;
// for instance, -3s here starts the animation from the 3rd second
stripe.style.transitionDelay = '-' + sec + 's';
stripe.classList.add('animate');
};
transition-timing-function
Timing function describes how the animation process is distributed along the time.
Will it start slowly and then go fast or vise versa.
That’s the most complicated property from the first sight. But it becomes very simple
if we devote a bit time to it.
That property accepts two kinds of values: a Bezier curve or steps. Let’s start from
the curve, as it’s used more often.
Bezier curve
The timing function can be set as a Bezier curve with 4 control points that satisfies
the conditions:
1. First control point: (0,0) .
2. Last control point: (1,1) .
3. For intermediate points values of x must be in the interval 0..1 , y can be
anything.
The syntax for a Bezier curve in CSS: cubic-bezier(x2, y2, x3, y3) . Here
we need to specify only 2nd and 3rd control points, because the 1st one is fixed to
(0,0) and the 4th one is (1,1) .
The timing function describes how fast the animation process goes in time.
● The x axis is the time: 0 – the starting moment, 1 – the last moment of
transition-duration .
● The y axis specifies the completion of the process: 0 – the starting value of the
property, 1 – the final value.
The simplest variant is when the animation goes uniformly, with the same linear
speed. That can be specified by the curve cubic-bezier(0, 0, 1, 1) .
…As we can see, it’s just a straight line. As the time ( x ) passes, the completion ( y )
of the animation steadily goes from 0 to 1 .
The train in the example below goes from left to right with the permanent speed
(click it):
https://plnkr.co/edit/BKXxsW1mgxIZvhcpUcj4?p=preview
.train {
left: 0;
transition: left 5s cubic-bezier(0, 0, 1, 1);
/* JavaScript sets left to 450px */
}
…And how can we show a train slowing down?
We can use another Bezier curve: cubic-bezier(0.0, 0.5, 0.5 ,1.0) .
The graph:
As we can see, the process starts fast: the curve soars up high, and then slower and
slower.
Here’s the timing function in action (click the train):
https://plnkr.co/edit/EBBtkTnI1l5096SHLcq8?p=preview
CSS:
.train {
left: 0;
transition: left 5s cubic-bezier(0, .5, .5, 1);
/* JavaScript sets left to 450px */
}
There are several built-in curves: linear , ease , ease-in , ease-out and
ease-in-out .
.train {
left: 0;
transition: left 5s ease-out;
/* transition: left 5s cubic-bezier(0, .5, .5, 1); */
}
.train {
left: 100px;
transition: left 5s cubic-bezier(.5, -1, .5, 2);
/* JavaScript sets left to 400px */
}
https://plnkr.co/edit/TjXgcacdDDsFyYHb4Lnl?p=preview
Why it happens – pretty obvious if we look at the graph of the given Bezier curve:
We moved the y coordinate of the 2nd point below zero, and for the 3rd point we
made put it over 1 , so the curve goes out of the “regular” quadrant. The y is out of
the “standard” range 0..1 .
That’s a “soft” variant for sure. If we put y values like -99 and 99 then the train
would jump out of the range much more.
But how to make the Bezier curve for a specific task? There are many tools. For
instance, we can do it on the site http://cubic-bezier.com/ .
Steps
Timing function steps(number of steps[, start/end]) allows to split
animation into steps.
Let’s see that in an example with digits.
Here’s a list of digits, without any animations, just as a source:
https://plnkr.co/edit/iyY2pj0vD8CcbFCuqnsI?p=preview
We’ll make the digits appear in a discrete way by making the part of the list outside
of the red “window” invisible and shifting the list to the left with each step.
There will be 9 steps, a step-move for each digit:
#stripe.animate {
transform: translate(-90%);
transition: transform 9s steps(9, start);
}
In action:
https://plnkr.co/edit/6VBxPjYIojjUL5vX8UvS?p=preview
The first argument of steps(9, start) is the number of steps. The transform
will be split into 9 parts (10% each). The time interval is automatically divided into 9
parts as well, so transition: 9s gives us 9 seconds for the whole animation – 1
second per digit.
The second argument is one of two words: start or end .
The start means that in the beginning of animation we need to do make the first
step immediately.
We can observe that during the animation: when we click on the digit it changes to
1 (the first step) immediately, and then changes in the beginning of the next second.
The alternative value end would mean that the change should be applied not in the
beginning, but at the end of each second.
So the process would go like this:
●
0s – 0
● 1s – -10% (first change at the end of the 1st second)
● 2s – -20%
● …
●
9s – -90%
Here’s step(9, end) in action (note the pause between the first digit change):
https://plnkr.co/edit/I3SoddMNBYDKxH2HeFak?p=preview
Event transitionend
It is widely used to do an action after the animation is done. Also we can join
animations.
For instance, the ship in the example below starts to swim there and back on click,
each time farther and farther to the right:
The animation is initiated by the function go that re-runs each time when the
transition finishes and flips the direction:
boat.onclick = function() {
//...
let times = 1;
function go() {
if (times % 2) {
// swim to the right
boat.classList.remove('back');
boat.style.marginLeft = 100 * times + 200 + 'px';
} else {
// swim to the left
boat.classList.add('back');
boat.style.marginLeft = 100 * times - 200 + 'px';
}
go();
boat.addEventListener('transitionend', function() {
times++;
go();
});
};
event.propertyName
The property that has finished animating. Can be good if we animate multiple
properties simultaneously.
event.elapsedTime
The time (in seconds) that the animation took, without transition-delay .
Keyframes
We can join multiple simple animations together using the @keyframes CSS rule.
It specifies the “name” of the animation and rules: what, when and where to animate.
Then using the animation property we attach the animation to the element and
specify additional parameters for it.
Here’s an example with explanations:
<div class="progress"></div>
<style>
@keyframes go-left-right { /* give it a name: "go-left-right" */
from { left: 0px; } /* animate from left: 0px */
to { left: calc(100% - 50px); } /* animate to left: 100%-50px */
}
.progress {
animation: go-left-right 3s infinite alternate;
/* apply the animation "go-left-right" to the element
duration 3 seconds
number of times: infinite
alternate direction every time
*/
position: relative;
border: 2px solid green;
width: 50px;
height: 20px;
background: lime;
}
</style>
Probably you won’t need @keyframes often, unless everything is in the constant
move on your sites.
Summary
CSS animations allow to smoothly (or not) animate changes of one or multiple CSS
properties.
They are good for most animation tasks. We’re also able to use JavaScript for
animations, the next chapter is devoted to that.
Limitations of CSS animations compared to JavaScript animations:
Merits Demerits
●
Simple things done simply. ●
JavaScript animations are
● Fast and lightweight for CPU. flexible. They can implement
any animation logic, like an
“explosion” of an element.
● Not just property changes. We
can create new elements in
JavaScript for purposes of
animation.
But in the next chapter we’ll do some JavaScript animations to cover more complex
cases.
✔ Tasks
Show the animation like on the picture below (click the plane):
● The picture grows on click from 40x24px to 400x240px (10 times larger).
● The animation takes 3 seconds.
● At the end output: “Done!”.
● During the animation process, there may be more clicks on the plane. They
shouldn’t “break” anything.
To solution
Modify the solution of the previous task Animate a plane (CSS) to make the plane
grow more than it’s original size 400x240px (jump out), and then return to that size.
To solution
Animated circle
importance: 5
The source document has an example of a circle with right styles, so the task is
precisely to do the animation right.
Open a sandbox for the task.
To solution
JavaScript animations
JavaScript animations can handle things that CSS can’t.
For instance, moving along a complex path, with a timing function different from
Bezier curves, or an animation on a canvas.
Using setInterval
For instance, changing style.left from 0px to 100px moves the element.
And if we increase it in setInterval , changing by 2px with a tiny delay, like 50
times per second, then it looks smooth. That’s the same principle as in the cinema:
24 frames per second is enough to make it look smooth.
}, 20);
// as timePassed goes from 0 to 2000
// left gets values from 0px to 400px
function draw(timePassed) {
train.style.left = timePassed / 5 + 'px';
}
Using requestAnimationFrame
That’s because they have different starting time, so “every 20ms” differs between
different animations. The intervals are not aligned. So we’ll have several
independent runs within 20ms .
setInterval(function() {
animate1();
animate2();
animate3();
}, 20)
These several independent redraws should be grouped together, to make the redraw
easier for the browser and hence load less CPU load and look smoother.
There’s one more thing to keep in mind. Sometimes when CPU is overloaded, or
there are other reasons to redraw less often (like when the browser tab is hidden), so
we really shouldn’t run it every 20ms .
That schedules the callback function to run in the closest time when the browser
wants to do animation.
If we do changes in elements in callback then they will be grouped together with
other requestAnimationFrame callbacks and with CSS animations. So there
will be one geometry recalculation and repaint instead of many.
The callback gets one argument – the time passed from the beginning of the
page load in microseconds. This time can also be obtained by calling
performance.now() .
Usually callback runs very soon, unless the CPU is overloaded or the laptop
battery is almost discharged, or there’s another reason.
The code below shows the time between first 10 runs for
requestAnimationFrame . Usually it’s 10-20ms:
<script>
let prev = performance.now();
let times = 0;
requestAnimationFrame(function measure(time) {
document.body.insertAdjacentHTML("beforeEnd", Math.floor(time - prev) + " ");
prev = time;
Structured animation
requestAnimationFrame(function animate(time) {
// timeFraction goes from 0 to 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
draw(progress); // draw it
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
duration
Total time of animation. Like, 1000 .
timing(timeFraction)
Timing function, like CSS-property transition-timing-function that gets the
fraction of time that passed ( 0 at start, 1 at the end) and returns the animation
completion (like y on the Bezier curve).
For instance, a linear function means that the animation goes on uniformly with the
same speed:
function linear(timeFraction) {
return timeFraction;
}
It’s graph:
draw(progress)
The function that takes the animation completion state and draws it. The value
progress=0 denotes the beginning animation state, and progress=1 – the end
state.
function draw(progress) {
train.style.left = progress + 'px';
}
Let’s animate the element width from 0 to 100% using our function.
https://plnkr.co/edit/5l241DCQmzPNofVr2wfk?p=preview
animate({
duration: 1000,
timing(timeFraction) {
return timeFraction;
},
draw(progress) {
elem.style.width = progress * 100 + '%';
}
});
Unlike CSS animation, we can make any timing function and any drawing function
here. The timing function is not limited by Bezier curves. And draw can go beyond
properties, create new elements for like fireworks animation or something.
Timing functions
Power of n
If we want to speed up the animation, we can use progress in the power n .
function quad(timeFraction) {
return Math.pow(timeFraction, 2)
}
The graph:
…Or the cubic curve or event greater n . Increasing the power makes it speed up
faster.
Here’s the graph for progress in the power 5 :
In action:
The arc
Function:
function circ(timeFraction) {
return 1 - Math.sin(Math.acos(timeFraction));
}
The graph:
The code:
Bounce
Imagine we are dropping a ball. It falls down, then bounces back a few times and
stops.
The bounce function does the same, but in the reverse order: “bouncing” starts
immediately. It uses few special coefficients for that:
function bounce(timeFraction) {
for (let a = 0, b = 1, result; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
}
}
}
In action:
Elastic animation
One more “elastic” function that accepts an additional parameter x for the “initial
range”.
Reversal: ease*
Sometimes we need to show the animation in the reverse order. That’s done with the
“easeOut” transform.
easeOut
In the “easeOut” mode the timing function is put into a wrapper
timingEaseOut :
Then the bounce will be not in the beginning, but at the end of the animation. Looks
even better:
https://plnkr.co/edit/opuSRjafyQ8Y41QXOolD?p=preview
Here we can see how the transform changes the behavior of the function:
If there’s an animation effect in the beginning, like bouncing – it will be shown at the
end.
In the graph above the regular bounce has the red color, and the easeOut bounce is
blue.
● Regular bounce – the object bounces at the bottom, then at the end sharply jumps
to the top.
● After easeOut – it first jumps to the top, then bounces there.
easeInOut
We also can show the effect both in the beginning and the end of the animation. The
transform is called “easeInOut”.
Given the timing function, we calculate the animation state like this:
bounceEaseInOut = makeEaseInOut(bounce);
In action, bounceEaseInOut :
https://plnkr.co/edit/F7NuLTRQblC8EgZr8ltV?p=preview
The “easeInOut” transform joins two graphs into one: easeIn (regular) for the first
half of the animation and easeOut (reversed) – for the second part.
The effect is clearly seen if we compare the graphs of easeIn , easeOut and
easeInOut of the circ timing function:
As we can see, the graph of the first half of the animation is the scaled down
easeIn , and the second half is the scaled down easeOut . As a result, the
animation starts and finishes with the same effect.
Instead of moving the element we can do something else. All we need is to write the
write the proper draw .
For animations that CSS can’t handle well, or those that need tight control,
JavaScript can help. JavaScript animations should be implemented via
requestAnimationFrame . That built-in method allows to setup a callback
function to run when the browser will be preparing a repaint. Usually that’s very
soon, but the exact time depends on the browser.
When a page is in the background, there are no repaints at all, so the callback won’t
run: the animation will be suspended and won’t consume resources. That’s great.
Here’s the helper animate function to setup most animations:
requestAnimationFrame(function animate(time) {
// timeFraction goes from 0 to 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
draw(progress); // draw it
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
Options:
● duration – the total animation time in ms.
●
timing – the function to calculate animation progress. Gets a time fraction from
0 to 1, returns the animation progress, usually from 0 to 1.
● draw – the function to draw the animation.
Surely we could improve it, add more bells and whistles, but JavaScript animations
are not applied on a daily basis. They are used to do something interesting and non-
standard. So you’d want to add the features that you need when you need them.
JavaScript animations can use any timing function. We covered a lot of examples
and transformations to make them even more versatile. Unlike CSS, we are not
limited to Bezier curves here.
The same is about draw : we can animate anything, not just CSS properties.
✔ Tasks
To solution
To solution
Web components
Web components is a set of standards to make self-contained components: custom
HTML-elements with their own properties and methods, encapsulated DOM and
styles.
As of now, these standards are under development. Some features are well-
supported and integrated into the modern HTML/DOM standard, while others are yet
in draft stage. You can try examples in any browser, Google Chrome is probably the
most up to date with these features. Guess, that’s because Google fellows are
behind many of the related specifications.
The whole component idea is nothing new. It’s used in many frameworks and
elsewhere.
Before we move to implementation details, take a look at this great achievement of
humanity:
That’s the International Space Station (ISS).
And this is how it’s made inside (approximately):
The International Space Station:
●
Consists of many components.
● Each component, in its turn, has many smaller details inside.
● The components are very complex, much more complicated than most websites.
●
Components are developed internationally, by teams from different countries,
speaking different languages.
Component architecture
The well known rule for developing complex software is: don’t make complex
software.
If something becomes complex – split it into simpler parts and connect in the most
obvious way.
A good architect is the one who can make the complex simple.
We can split user interface into visual components: each of them has own place on
the page, can “do” a well-described task, and is separate from the others.
Let’s take a look at a website, for example Twitter.
1. Top navigation.
2. User info.
3. Follow suggestions.
4. Submit form.
5. (and also 6, 7) – messages.
How do we decide, what is a component? That comes from intuition, experience and
common sense. Usually it’s a separate visual entity that we can describe in terms of
what it does and how it interacts with the page. In the case above, the page has
blocks, each of them plays its own role, it’s logical to make these components.
A component has:
● its own JavaScript class.
● DOM structure, managed solely by its class, outside code doesn’t access it
(“encapsulation” principle).
● CSS styles, applied to the component.
● API: events, class methods etc, to interact with other components.
There exist many frameworks and development methodologies to build them, each
one with its own bells and whistles. Usually, special CSS classes and conventions
are used to provide “component feel” – CSS scoping and DOM encapsulation.
“Web components” provide built-in browser capabilities for that, so we don’t have to
emulate them any more.
●
Custom elements – to define custom HTML elements.
●
Shadow DOM – to create an internal DOM for the component, hidden from the
others.
● CSS Scoping – to declare styles that only apply inside the Shadow DOM of the
component.
● Event retargeting and other minor stuff to make custom components better fit
the development.
In the next chapter we’ll go into details of “Custom Elements” – the fundamental and
well-supported feature of web components, good on its own.
Custom elements
We can create custom HTML elements, described by our class, with its own
methods and properties, events and so on.
Once an custom element is defined, we can use it on par with built-in HTML
elements.
That’s great, as HTML dictionary is rich, but not infinite. There are no <easy-
tabs> , <sliding-carousel> , <beautiful-upload> … Just think of any
other tag we might need.
We can define them with a special class, and then use as if they were always a part
of HTML.
First we’ll create autonomous elements, and then customized built-in ones.
To create a custom element, we need to tell the browser several details about it: how
to show it, what to do when the element is added or removed to page, etc.
That’s done by making a class with special methods. That’s easy, as there are only
few methods, and all of them are optional.
connectedCallback() {
// browser calls it when the element is added to the document
// (can be called many times if an element is repeatedly added/removed)
}
disconnectedCallback() {
// browser calls it when the element is removed from the document
// (can be called many times if an element is repeatedly added/removed)
}
adoptedCallback() {
// called when the element is moved to a new document
// (happens in document.adoptNode, very rarely used)
}
// let the browser know that <my-element> is served by our new class
customElements.define("my-element", MyElement);
Now for any HTML elements with tag <my-element> , an instance of MyElement
is created, and the aforementioned methods are called. We also can
document.createElement('my-element') in JavaScript.
Custom element name must contain a hyphen -
Custom element name must have a hyphen - , e.g. my-element and super-
button are valid names, but myelement is not.
That’s to ensure that there are no name conflicts between built-in and custom
HTML elements.
Example: “time-formatted”
For example, there already exists <time> element in HTML, for date/time. But it
doesn’t do any formatting by itself.
Let’s create <time-formatted> element that displays the time in a nice,
language-aware format:
<script>
class TimeFormatted extends HTMLElement { // (1)
connectedCallback() {
let date = new Date(this.getAttribute('datetime') || Date.now());
The reason is simple: when constructor is called, it’s yet too early. The
element instance is created, but not populated yet. The browser did not yet
process/assign attributes at this stage: calls to getAttribute would return
null . So we can’t really render there.
Besides, if you think about it, that’s better performance-wise – to delay the work
until it’s really needed.
Observing attributes
<script>
class TimeFormatted extends HTMLElement {
render() { // (1)
let date = new Date(this.getAttribute('datetime') || Date.now());
connectedCallback() { // (2)
if (!this.rendered) {
this.render();
this.rendered = true;
}
}
customElements.define("time-formatted", TimeFormatted);
</script>
<script>
setInterval(() => elem.setAttribute('datetime', new Date()), 1000); // (5)
</script>
7:14:48 PM
Rendering order
When HTML parser builds the DOM, elements are processed one after another,
parents before children. E.g. if we have <outer><inner></inner></outer> ,
then <outer> element is created and connected to DOM first, and then <inner> .
connectedCallback() {
alert(this.innerHTML); // empty (*)
}
});
</script>
<user-info>John</user-info>
That’s exactly because there are no children on that stage, the DOM is unfinished.
HTML parser connected the custom element <user-info> , and will now proceed
to its children, but just didn’t yet.
If we’d like to pass information to custom element, we can use attributes. They are
available immediately.
Or, if we really need the children, we can defer access to them with zero-delay
setTimeout .
This works:
<script>
customElements.define('user-info', class extends HTMLElement {
connectedCallback() {
setTimeout(() => alert(this.innerHTML)); // John (*)
}
});
</script>
<user-info>John</user-info>
Now the alert in line (*) shows “John”, as we run it asynchronously, after the
HTML parsing is complete. We can process children if needed and finish the
initialization.
On the other hand, this solution is also not perfect. If nested custom elements also
use setTimeout to initialize themselves, then they queue up: the outer
setTimeout triggers first, and then the inner one.
So the outer element finishes the initialization before the inner one.
Let’s demonstrate that on example:
<script>
customElements.define('user-info', class extends HTMLElement {
connectedCallback() {
alert(`${this.id} connected.`);
setTimeout(() => alert(`${this.id} initialized.`));
}
});
</script>
<user-info id="outer">
<user-info id="inner"></user-info>
</user-info>
Output order:
1. outer connected.
2. inner connected.
3. outer initialized.
4. inner initialized.
We can clearly see that the outer element does not wait for the inner one.
There’s no built-in callback that triggers after nested elements are ready. But we can
implement such thing on our own. For instance, inner elements can dispatch events
like initialized , and outer ones can listen and react on them.
But such things can be important. E.g, a search engine would be interested to know
that we actually show a time. And if we’re making a special kind of button, why not
reuse the existing <button> functionality?
We can extend and customize built-in elements by inheriting from their classes.
For example, buttons are instances of HTMLButtonElement , let’s build upon it.
There exist different tags that share the same class, that’s why it’s needed.
3. At the end, to use our custom element, insert a regular <button> tag, but add
is="hello-button" to it:
<button is="hello-button">...</button>
<script>
// The button that says "hello" on click
class HelloButton extends HTMLButtonElement {
constructor() {
super();
this.addEventListener('click', () => alert("Hello!"));
}
}
Click me Disabled
Our new button extends the built-in one. So it keeps the same styles and standard
features like disabled attribute.
References
● HTML Living Standard: https://html.spec.whatwg.org/#custom-elements .
● Compatiblity: https://caniuse.com/#feat=custom-elements .
Summary
Definition scheme:
class MyElement extends HTMLElement {
constructor() { super(); /* ... */ }
connectedCallback() { /* ... */ }
disconnectedCallback() { /* ... */ }
static get observedAttributes() { return [/* ... */]; }
attributeChangedCallback(name, oldValue, newValue) { /* ... */ }
adoptedCallback() { /* ... */ }
}
customElements.define('my-element', MyElement);
/* <my-element> */
Custom elements are well-supported among browsers. Edge is a bit behind, but
there’s a polyfill https://github.com/webcomponents/webcomponentsjs .
✔ Tasks
Usage:
<live-timer id="elem"></live-timer>
<script>
elem.addEventListener('tick', event => console.log(event.detail));
</script>
Demo:
7:14:48 PM
To solution
Shadow DOM
Shadow DOM serves for encapsulation. It allows a component to have its very own
“shadow” DOM tree, that can’t be accidentally accessed from the main document,
may have local style rules, and more.
Did you ever think how complex browser controls are created and styled?
Such as <input type="range"> :
The browser uses DOM/CSS internally to draw them. That DOM structure is
normally hidden from us, but we can see it in developer tools. E.g. in Chrome, we
need to enable in Dev Tools “Show user agent shadow DOM” option.
We can’t get built-in shadow DOM elements by regular JavaScript calls or selectors.
These are not regular children, but a powerful encapsulation technique.
In the example above, we can see a useful attribute pseudo . It’s non-standard,
exists for historical reasons. We can use it style subelements with CSS, like this:
<style>
/* make the slider track red */
input::-webkit-slider-runnable-track {
background: red;
}
</style>
<input type="range">
Further on, we’ll use the modern shadow DOM standard, covered by DOM spec
other related specifications.
Shadow tree
1. Light tree – a regular DOM subtree, made of HTML children. All subtrees that
we’ve seen in previous chapters were “light”.
2. Shadow tree – a hidden DOM subtree, not reflected in HTML, hidden from prying
eyes.
If an element has both, then the browser renders only the shadow tree. But we can
setup a kind of composition between shadow and light trees as well. We’ll see the
details later in the chapter Shadow DOM slots, composition.
Shadow tree can be used in Custom Elements to hide component internals and
apply component-local styles.
For example, this <show-hello> element hides its internal DOM in shadow tree:
<script>
customElements.define('show-hello', class extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = `<p>
Hello, ${this.getAttribute('name')}
</p>`;
}
});
</script>
<show-hello name="John"></show-hello>
Hello, John
That’s how the resulting DOM looks in Chrome dev tools, all the content is under
“#shadow-root”:
The mode option sets the encapsulation level. It must have any of two values:
● "open" – the shadow root is available as elem.shadowRoot .
The element with a shadow root is called a “shadow tree host”, and is available as
the shadow root host property:
Encapsulation
For example:
<style>
/* document style won't apply to the shadow tree inside #elem (1) */
p { color: red; }
</style>
<div id="elem"></div>
<script>
elem.attachShadow({mode: 'open'});
// shadow tree has its own style (2)
elem.shadowRoot.innerHTML = `
<style> p { font-weight: bold; } </style>
<p>Hello, John!</p>
`;
// <p> is only visible from queries inside the shadow tree (3)
alert(document.querySelectorAll('p').length); // 0
alert(elem.shadowRoot.querySelectorAll('p').length); // 1
</script>
1. The style from the document does not affect the shadow tree.
2. …But the style from the inside works.
3. To get elements in shadow tree, we must query from inside the tree.
References
● DOM: https://dom.spec.whatwg.org/#shadow-trees
● Compatibility: https://caniuse.com/#feat=shadowdomv1
Summary
Shadow DOM, if exists, is rendered by the browser instead of so-called “light DOM”
(regular children). In the chapter Shadow DOM slots, composition we’ll see how to
compose them.
Template element
A built-in <template> element serves as a storage for HTML markup templates.
The browser ignores it contents, only checks for syntax validity, but we can access
and use it in JavaScript, to create other elements.
In theory, we could create any invisible element somewhere in HTML for HTML
markup storage purposes. What’s special about <template> ?
First, its content can be any valid HTML, even if it normally requires a proper
enclosing tag.
<template>
<tr>
<td>Contents</td>
</tr>
</template>
Usually, if we try to put <tr> inside, say, a <div> , the browser detects the invalid
DOM structure and “fixes” it, adds <table> around. That’s not what we want. On
the other hand, <template> keeps exactly what we place there.
<template>
<style>
p { font-weight: bold; }
</style>
<script>
alert("Hello");
</script>
</template>
The browser considers <template> content “out of the document”: styles are not
applied, scripts are not executed, <video autoplay> is not run, etc.
The content becomes live (styles apply, scripts run etc) when we insert it into the
document.
Inserting template
We can treat it as any other DOM node, except one special property: when we insert
it somewhere, its children are inserted instead.
For example:
<template id="tmpl">
<script>
alert("Hello");
</script>
<div class="message">Hello, world!</div>
</template>
<script>
let elem = document.createElement('div');
document.body.append(elem);
// Now the script from <template> runs
</script>
Let’s rewrite a Shadow DOM example from the previous chapter using
<template> :
<template id="tmpl">
<style> p { font-weight: bold; } </style>
<p id="message"></p>
</template>
elem.shadowRoot.append(tmpl.content.cloneNode(true)); // (*)
Click me
<div id="elem">
#shadow-root
<style> p { font-weight: bold; } </style>
<p id="message"></p>
</div>
Summary
To summarize:
●
<template> content can be any syntactically correct HTML.
● <template> content is considered “out of the document”, so it doesn’t affect
anything.
● We can access template.content from JavaScript, clone it to reuse in a new
component.
The <template> element does not feature any iteration mechanisms, data binding
or variable substitutions, but we can implement those on top of it.
Shadow DOM slots, composition
Many types of components, such as tabs, menus, image galleries, and so on, need
the content to render.
Just like built-in browser <select> expects <option> items, our <custom-
tabs> may expect the actual tab content to be passed. And a <custom-menu>
may expect menu items.
The code that makes use of <custom-menu> can look like this:
<custom-menu>
<title>Candy menu</title>
<item>Lollipop</item>
<item>Fruit Toast</item>
<item>Cup Cake</item>
</custom-menu>
…Then our component should render it properly, as a nice menu with given title and
items, handle menu events, etc.
How to implement it?
We could try to analyze the element content and dynamically copy-rearrange DOM
nodes. That’s possible, but if we’re moving elements to shadow DOM, then CSS
styles from the document do not apply in there, so the visual styling may be lost.
Also that requires some coding.
Luckily, we don’t have to. Shadow DOM supports <slot> elements, that are
automatically filled by the content from light DOM.
Named slots
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div>Name:
<slot name="username"></slot>
</div>
<div>Birthday:
<slot name="birthday"></slot>
</div>
`;
}
});
</script>
<user-card>
<span slot="username">John Smith</span>
<span slot="birthday">01.01.2001</span>
</user-card>
Then the browser performs “composition”: it takes elements from the light DOM and
renders them in corresponding slots of the shadow DOM. At the end, we have
exactly what we want – a generic component that can be filled with data.
Here’s the DOM structure after the script, not taking composition into account:
<user-card>
#shadow-root
<div>Name:
<slot name="username"></slot>
</div>
<div>Birthday:
<slot name="birthday"></slot>
</div>
<span slot="username">John Smith</span>
<span slot="birthday">01.01.2001</span>
</user-card>
There’s nothing odd here. We created the shadow DOM, so here it is. Now the
element has both light and shadow DOM.
For rendering purposes, for each <slot name="..."> in shadow DOM, the
browser looks for slot="..." with the same name in the light DOM. These
elements are rendered inside the slots:
The result is called “flattened” DOM:
<user-card>
#shadow-root
<div>Name:
<slot name="username">
<!-- slotted element is inserted into the slot as a whole -->
<span slot="username">John Smith</span>
</slot>
</div>
<div>Birthday:
<slot name="birthday">
<span slot="birthday">01.01.2001</span>
</slot>
</div>
</user-card>
…But the “flattened” DOM is only created for rendering and event-handling
purposes. That’s how things are shown. The nodes are actually not moved around!
That can be easily checked if we run querySelector : nodes are still at their
places.
// light DOM <span> nodes are still at the same place, under `<user-card>`
alert( document.querySelector('user-card span').length ); // 2
It may look bizarre, but for shadow DOM with slots we have one more “DOM level”,
the “flattened” DOM – result of slot insertion. The browser renders it and uses for
style inheritance, event propagation. But JavaScript still sees the document “as is”,
before flattening.
⚠ Only top-level children may have slot="…" attribute
The slot="..." attribute is only valid for direct children of the shadow host (in
our example, <user-card> element). For nested elements it’s ignored.
For example, the second <span> here is ignored (as it’s not a top-level child of
<user-card> ):
<user-card>
<span slot="username">John Smith</span>
<div>
<!-- bad slot, not top-level: -->
<span slot="birthday">01.01.2001</span>
</div>
</user-card>
If we put something inside a <slot> , it becomes the fallback content. The browser
shows it if there’s no corresponding filler in light DOM.
<div>Name:
<slot name="username">Anonymous</slot>
</div>
Default slot
The first <slot> in shadow DOM that doesn’t have a name is a “default” slot. It
gets all nodes from the light DOM that aren’t slotted elsewhere.
For example, let’s add the default slot to our <user-card> that collects any
unslotted information about the user:
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div>Name:
<slot name="username"></slot>
</div>
<div>Birthday:
<slot name="birthday"></slot>
</div>
<fieldset>
<legend>Other information</legend>
<slot></slot>
</fieldset>
`;
}
});
</script>
<user-card>
<div>I like to swim.</div>
<span slot="username">John Smith</span>
<span slot="birthday">01.01.2001</span>
<div>...And play volleyball too!</div>
</user-card>
All the unslotted light DOM content gets into the “Other information” fieldset.
Elements are appended to a slot one after another, so both unslotted pieces of
information are in the default slot together.
<user-card>
#shadow-root
<div>Name:
<slot name="username">
<span slot="username">John Smith</span>
</slot>
</div>
<div>Birthday:
<slot name="birthday">
<span slot="birthday">01.01.2001</span>
</slot>
</div>
<fieldset>
<legend>About me</legend>
<slot>
<div>Hello</div>
<div>I am John!</div>
</slot>
</fieldset>
</user-card>
Menu example
<custom-menu>
<span slot="title">Candy menu</span>
<li slot="item">Lollipop</li>
<li slot="item">Fruit Toast</li>
<li slot="item">Cup Cake</li>
</custom-menu>
<template id="tmpl">
<style> /* menu styles */ </style>
<div class="menu">
<slot name="title"></slot>
<ul><slot name="item"></slot></ul>
</div>
</template>
<custom-menu>
#shadow-root
<style> /* menu styles */ </style>
<div class="menu">
<slot name="title">
<span slot="title">Candy menu</span>
</slot>
<ul>
<slot name="item">
<li slot="item">Lollipop</li>
<li slot="item">Fruit Toast</li>
<li slot="item">Cup Cake</li>
</slot>
</ul>
</div>
</custom-menu>
One might notice that, in a valid DOM, <li> must be a direct child of <ul> . But
that’s flattened DOM, it describes how the component is rendered, such thing
happens naturally here.
We just need to add a click handler to open/close the list, and the <custom-
menu> is ready:
// we can't select light DOM nodes, so let's handle clicks on the slot
this.shadowRoot.querySelector('slot[name="title"]').onclick = () => {
// open/close the menu
this.shadowRoot.querySelector('.menu').classList.toggle('closed');
};
}
});
Candy menu
Lollipop
Fruit Toast
Cup Cake
Of course, we can add more functionality to it: events, methods and so on.
Monitoring slots
For example, here the menu item is inserted dynamically after 1 second, and the title
changes after 2 seconds:
<custom-menu id="menu">
<span slot="title">Candy menu</span>
</custom-menu>
<script>
customElements.define('custom-menu', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div class="menu">
<slot name="title"></slot>
<ul><slot name="item"></slot></ul>
</div>`;
setTimeout(() => {
menu.insertAdjacentHTML('beforeEnd', '<li slot="item">Lollipop</li>')
}, 1000);
setTimeout(() => {
menu.querySelector('[slot="title"]').innerHTML = "New menu";
}, 2000);
</script>
1. At initialization:
Please note: there’s no slotchange event after 2 seconds, when the content of
slot="title" is modified. That’s because there’s no slot change. We modify the
content inside the slotted element, that’s another thing.
If we’d like to track internal modifications of light DOM from JavaScript, that’s also
possible using a more generic mechanism: MutationObserver.
Slot API
As we’ve seen before, JavaScript looks at the “real” DOM, without flattening. But, if
the shadow tree has {mode: 'open'} , then we can figure out which elements
assigned to a slot and, vise-versa, the slot by the element inside it:
● node.assignedSlot – returns the <slot> element that the node is
assigned to.
● slot.assignedNodes({flatten: true/false}) – DOM nodes,
assigned to the slot. The flatten option is false by default. If explicitly set to
true , then it looks more deeply into the flattened DOM, returning nested slots in
case of nested components and the fallback content if no node assigned.
●
slot.assignedElements({flatten: true/false}) – DOM elements,
assigned to the slot (same as above, but only element nodes).
These methods are useful when we need not just show the slotted content, but also
track it in JavaScript.
<custom-menu id="menu">
<span slot="title">Candy menu</span>
<li slot="item">Lollipop</li>
<li slot="item">Fruit Toast</li>
</custom-menu>
<script>
customElements.define('custom-menu', class extends HTMLElement {
items = []
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div class="menu">
<slot name="title"></slot>
<ul><slot name="item"></slot></ul>
</div>`;
// slottable is added/removed/replaced
this.shadowRoot.firstElementChild.addEventListener('slotchange', e => {
let slot = e.target;
if (slot.name == 'item') {
this.items = slot.assignedElements().map(elem => elem.textContent);
alert("Items: " + this.items);
}
});
}
});
Summary
The process of rendering slotted elements inside their slots is called “composition”.
The result is called a “flattened DOM”.
Composition does not really move nodes, from JavaScript point of view the DOM is
still same.
JavaScript can access slots using methods:
● slot.assignedNodes/Elements() – returns nodes/elements inside the
slot .
●
node.assignedSlot – the reverse meethod, returns slot by a node.
If we’d like to know what we’re showing, we can track slot contents using:
●
slotchange event – triggers the first time a slot is filled, and on any
add/remove/replace operation of the slotted element, but not its children. The slot
is event.target .
● MutationObserver to go deeper into slot content, watch changes inside it.
Now, as we have elements from light DOM in the shadow DOM, let’s see how to
style them properly. The basic rule is that shadow elements are styled inside, and
light elements – outside, but there are notable exceptions.
We’ll see the details in the next chapter.
As a general rule, local styles work only inside the shadow tree, and document styles
work outside of it. But there are few exceptions.
:host
The :host selector allows to select the shadow host (the element containing the
shadow tree).
For instance, we’re making <custom-dialog> element that should be centered.
For that we need to style the <custom-dialog> element itself.
<template id="tmpl">
<style>
/* the style will be applied from inside to the custom-dialog element */
:host {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: inline-block;
border: 1px solid red;
padding: 10px;
}
</style>
<slot></slot>
</template>
<script>
customElements.define('custom-dialog', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
}
});
</script>
<custom-dialog>
Hello!
</custom-dialog>
Hello!
Cascading
The shadow host ( <custom-dialog> itself) resides in the light DOM, so it’s
affected by the main CSS cascade.
If there’s a property styled both in :host locally, and in the document, then the
document style takes precedence.
For instance, if in the document we had:
<style>
custom-dialog {
padding: 0;
}
</style>
It’s very convenient, as we can setup “default” styles in the component :host rule,
and then easily override them in the document.
:host(selector)
Same as :host , but applied only if the shadow host matches the selector .
For example, we’d like to center the <custom-dialog> only if it has centered
attribute:
<template id="tmpl">
<style>
:host([centered]) {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
:host {
display: inline-block;
border: 1px solid red;
padding: 10px;
}
</style>
<slot></slot>
</template>
<script>
customElements.define('custom-dialog', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
}
});
</script>
<custom-dialog centered>
Centered!
</custom-dialog>
<custom-dialog>
Not centered.
</custom-dialog>
Not centered.
Centered!
Now the additional centering styles are only applied to the first dialog <custom-
dialog centered> .
:host-context(selector)
Same as :host , but applied only if the shadow host or any of its ancestors in the
outer document matches the selector .
To summarize, we can use :host -family of selectors to style the main element of
the component, depending on the context. These styles (unless !important ) can
be overridden by the document.
Slotted elements come from light DOM, so they use document styles. Local styles do
not affect slotted content.
In the example below, slotted <span> is bold, as per document style, but does not
take background from the local style:
<style>
span { font-weight: bold }
</style>
<user-card>
<div slot="username"><span>John Smith</span></div>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style>
span { background: red; }
</style>
Name: <slot name="username"></slot>
`;
}
});
</script>
Name:
John Smith
The result is bold, but not red.
If we’d like to style slotted elements in our component, there are two choices.
First, we can style the <slot> itself and rely on CSS inheritance:
<user-card>
<div slot="username"><span>John Smith</span></div>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style>
slot[name="username"] { font-weight: bold; }
</style>
Name: <slot name="username"></slot>
`;
}
});
</script>
Name:
John Smith
<user-card>
<div slot="username">
<div>John Smith</div>
</div>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style>
::slotted(div) { border: 1px solid red; }
</style>
Name: <slot name="username"></slot>
`;
}
});
</script>
Name:
John Smith
Please note, ::slotted selector can’t descend any further into the slot. These
selectors are invalid:
::slotted(div span) {
/* our slotted <div> does not match this */
}
::slotted(div) p {
/* can't go inside light DOM */
}
Then, we can declare this property in the outer document for <user-card> :
user-card {
--user-card-field-color: green;
}
Custom CSS properties pierce through shadow DOM, they are visible everywhere,
so the inner .field rule will make use of it.
<style>
user-card {
--user-card-field-color: green;
}
</style>
<template id="tmpl">
<style>
.field {
color: var(--user-card-field-color, black);
}
</style>
<div class="field">Name: <slot name="username"></slot></div>
<div class="field">Birthday: <slot name="birthday"></slot></div>
</template>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.append(document.getElementById('tmpl').content.cloneNode(true))
}
});
</script>
<user-card>
<span slot="username">John Smith</span>
<span slot="birthday">01.01.2001</span>
</user-card>
Summary
When CSS properties conflict, normally document styles have precedence, unless
the property is labelled as !important . Then local styles have precedence.
CSS custom properties pierce through shadow DOM. They are used as “hooks” to
style the component:
1. The component uses a custom CSS property to style key elements, such as
var(--component-name-title, <default value>) .
2. Component author publishes these properties for developers, they are same
important as other public component methods.
3. When a developer wants to style a title, they assign --component-name-
title CSS property for the shadow host or above.
4. Profit!
Events that happen in shadow DOM have the host element as the target, when
caught outside of the component.
<user-card></user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<p>
<button>Click me</button>
</p>`;
this.shadowRoot.firstElementChild.onclick =
e => alert("Inner target: " + e.target.tagName);
}
});
document.onclick =
e => alert("Outer target: " + e.target.tagName);
</script>
Click me
Event retargeting is a great thing to have, because the outer document doesn’t have
no know about component internals. From its point of view, the event happened on
<user-card> .
Retargeting does not occur if the event occurs on a slotted element, that
physically lives in the light DOM.
<user-card id="userCard">
<span slot="username">John Smith</span>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div>
<b>Name:</b> <slot name="username"></slot>
</div>`;
this.shadowRoot.firstElementChild.onclick =
e => alert("Inner target: " + e.target.tagName);
}
});
If a click happens on "John Smith" , for both inner and outer handlers the target
is <span slot="username"> . That’s an element from the light DOM, so no
retargeting.
On the other hand, if the click occurs on an element originating from shadow DOM,
e.g. on <b>Name</b> , then, as it bubbles out of the shadow DOM, its
event.target is reset to <user-card> .
Bubbling, event.composedPath()
The full path to the original event target, with all the shadow elements, can be
obtained using event.composedPath() . As we can see from the name of the
method, that path is taken after the composition.
<user-card id="userCard">
#shadow-root
<div>
<b>Name:</b>
<slot name="username">
<span slot="username">John Smith</span>
</slot>
</div>
</user-card>
If the shadow tree was created with {mode: 'closed'} , then the composed
path starts from the host: user-card and upwards.
That’s the similar principle as for other methods that work with shadow DOM.
Internals of closed trees are completely hidden.
event.composed
Most events successfully bubble through a shadow DOM boundary. There are few
events that do not.
This is governed by the composed event object property. If it’s true , then the
event does cross the boundary. Otherwise, it only can be caught from inside the
shadow DOM.
All touch events and pointer events also have composed: true .
Custom events
When we dispatch custom events, we need to set both bubbles and composed
properties to true for it to bubble up and out of the component.
For example, here we create div#inner in the shadow DOM of div#outer and
trigger two events on it. Only the one with composed: true makes it outside to
the document:
<div id="outer"></div>
<script>
outer.attachShadow({mode: 'open'});
/*
div(id=outer)
#shadow-dom
div(id=inner)
*/
inner.dispatchEvent(new CustomEvent('test', {
bubbles: true,
composed: true,
detail: "composed"
}));
inner.dispatchEvent(new CustomEvent('test', {
bubbles: true,
composed: false,
detail: "not composed"
}));
</script>
Summary
Events only cross shadow DOM boundaries if their composed flag is set to true .
These events can be caught only on elements within the same DOM.
If we dispatch a CustomEvent , then we should explicitly set composed: true .
Please note that in case of nested components, one shadow DOM may be nested
into another. In that case composed events bubble through all shadow DOM
boundaries. So, if an event is intended only for the immediate enclosing component,
we can also dispatch it on the shadow host and set composed: false . Then it’s
out of the component shadow DOM, but won’t bubble up to higher-level DOM.
Regular expressions
Regular expressions is a powerful way of doing search and replace in strings.
In JavaScript regular expressions are implemented using objects of a built-in
RegExp class and integrated with strings.
Please note that regular expressions vary between programming languages. In this
tutorial we concentrate on JavaScript. Of course there’s a lot in common, but they
are a somewhat different in Perl, Ruby, PHP etc.
Slashes "/" tell JavaScript that we are creating a regular expression. They play the
same role as quotes for strings.
Usage
Here’s an example:
The str.search method looks for the pattern /love/ and returns the position
inside the string. As we might guess, /love/ is the simplest possible pattern. What
it does is a simple substring search.
The code above is the same as:
But that’s only for now. Soon we’ll create more complex regular expressions with
much more searching power.
Colors
From here on the color scheme is:
● regexp – red
●
string (where we search) – blue
●
result – green
When to use new RegExp ?
Normally we use the short syntax /.../ . But it does not support variable
insertions ${...} .
On the other hand, new RegExp allows to construct a pattern dynamically from
a string, so it’s more flexible.
Here’s an example of a dynamically generated regexp:
Flags
i
With this flag the search is case-insensitive: no difference between A and a (see
the example below).
g
With this flag the search looks for all matches, without it – only the first one (we’ll see
uses in the next chapter).
m
Multiline mode (covered in the chapter Multiline mode, flag "m").
s
“Dotall” mode, allows . to match newlines (covered in the chapter Character
classes).
u
Enables full unicode support. The flag enables correct processing of surrogate pairs.
More about that in the chapter Unicode: flag "u".
y
Sticky mode (covered in the chapter Sticky flag "y", searching at position)
So the i flag already makes regular expressions more powerful than a simple
substring search. But there’s so much more. We’ll cover other flags and features in
the next chapters.
Summary
● A regular expression consists of a pattern and optional flags: g , i , m , u , s ,
y.
● Without flags and special symbols that we’ll study later, the search by a regexp is
the same as a substring search.
●
The method str.search(regexp) returns the index where the match is found
or -1 if there’s no match. In the next chapter we’ll see other methods.
Recipes
Methods become much easier to understand if we separate them by their use in real-
life tasks.
So, here are general recipes, the details to follow:
Now you can continue reading this chapter to get the details about every method…
But if you’re reading for the first time, then you probably want to know more about
regexps. So you can move to the next chapter, and then return here if something
about a method is unclear.
str.search(reg)
We’ve seen this method already. It returns the position of the first match or -1 if
none found:
We can’t find next matches using search , there’s just no syntax for that. But there
are other methods that can.
The behavior of str.match varies depending on whether reg has g flag or not.
First, if there’s no g flag, then str.match(reg) looks for the first match only.
The result is an array with that match and additional properties:
● index – the position of the match inside the string,
● input – the subject string.
For instance:
For instance:
Due to the i flag the search is case-insensitive, so it finds JavaScript . The part
of the match that corresponds to SCRIPT becomes a separate array item.
So, this method is used to find one full match with all details.
When there’s a "g" flag, then str.match returns an array of all matches. There
are no additional properties in that array, and parentheses do not create any
elements.
For instance:
So, with g flag str.match returns a simple array of all matches, without
details.
If we want to get information about match positions and contents of parentheses
then we should use matchAll method that we’ll cover below.
Please note, that’s important. If there are no matches, the result is not an empty
array, but null .
str.matchAll(regexp)
The method str.matchAll(regexp) is used to find all matches with all details.
For instance:
In practice, if we need all matches, then for..of works, so it’s not a problem.
alert(firstMatch); // Javascript
str.split(regexp|substr, limit)
Splits the string using the regexp (or a substring) as a delimiter.
We already used split with strings, like this:
str.replace(str|reg, str|func)
This is a generic method for searching and replacing, one of most useful ones. The
swiss army knife for searching and replacing.
We can use it without regexps, to search and replace a substring:
When the first argument of replace is a string, it only looks for the first
match.
You can see that in the example above: only the first "-" is replaced by ":" .
To find all dashes, we need to use not the string "-" , but a regexp /-/g , with an
obligatory g flag:
The second argument is a replacement string. We can use special characters in it:
Symbol Inserts
$$ "$"
if n is a 1-2 digit number, then it means the contents of n-th parentheses counting from left to right,
$n
otherwise it means a parentheses with the given name
For instance if we use $& in the replacement string, that means “put the whole
match here”.
Let’s use it to prepend all entries of "John" with "Mr." :
Quite often we’d like to reuse parts of the source string, recombine them in the
replacement or wrap into something.
To do so, we should:
For instance:
For situations that require “smart” replacements, the second argument can be
a function.
It will be called for each match, and its result will be inserted as a replacement.
For instance:
let i = 0;
The function is called with arguments func(str, p1, p2, ..., pn,
offset, input, groups) :
If there are no parentheses in the regexp, then there are only 3 arguments:
func(str, offset, input) .
In the example below there are two parentheses, so replacer is called with 5
arguments: str is the full match, then parentheses, and then offset and
input :
regexp.exec(str)
The regexp.exec method is the most flexible searching method of all. Unlike
previous methods, exec should be called on a regexp, rather than on a string.
We could use it to get all matches with their positions and parentheses groups in a
loop, instead of matchAll :
let result;
let result;
Now, starting from the given position 13 , there’s only one match.
regexp.test(str)
For instance, here we call regexp.test twice on the same text, and the
second time fails:
Summary
Their abilities and methods overlap quite a bit, we can do the same by different calls.
Sometimes that may cause confusion when starting to learn the language.
Then please refer to the recipes at the beginning of this chapter, as they provide
solutions for the majority of regexp-related tasks.
Character classes
Consider a practical task – we have a phone number "+7(903)-123-45-67" ,
and we need to turn it into pure numbers: 79035419441 .
To do so, we can find and remove anything that’s not a number. Character classes
can help with that.
A character class is a special notation that matches any symbol from a certain set.
For the start, let’s explore a “digit” class. It’s written as \d . We put it in the pattern,
that means “any single digit”.
For instance, the let’s find the first digit in the phone number:
alert( str.match(reg) ); // 7
Without the flag g , the regular expression only looks for the first match, that is the
first digit \d .
That was a character class for digits. There are other character classes as well.
For instance, CSS\d matches a string CSS with a digit after it:
let str = "CSS4 is cool";
let reg = /CSS\d/
Word boundary: \b
For instance, \bJava\b matches Java in the string Hello, Java! , but not in
the script Hello, JavaScript! .
The boundary has “zero width” in a sense that usually a character class means a
character in the result (like a wordly character or a digit), but not in this case.
When the pattern contains \b , it tests that the position in string is a word boundary,
that is one of three variants:
● Immediately before is \w , and immediately after – not \w , or vise versa.
●
At string start, and the first string character is \w .
●
At string end, and the last string character is \w .
For instance, in the string Hello, Java! the following positions match \b :
So it matches \bHello\b , because:
Pattern \bJava\b also matches. But not \bHell\b (because there’s no word
boundary after l ) and not Java!\b (because the exclamation sign is not a wordly
character, so there’s no word boundary after it).
Once again let’s note that \b makes the searching engine to test for the boundary,
so that Java\b finds Java only when followed by a word boundary, but it does not
add a letter to the result.
Later we’ll come by Unicode character classes that allow to solve the similar task
for different languages.
Inverse classes
For every character class there exists an “inverse class”, denoted with the same
letter, but uppercased.
The “reverse” means that it matches all other characters, for instance:
\D
Non-digit: any character except \d , for instance a letter.
\S
Non-space: any character except \s , for instance a letter.
\W
Non-wordly character: anything but \w .
\B
Non-boundary: a test reverse to \b .
In the beginning of the chapter we saw how to get all digits from the phone
+7(903)-123-45-67 .
An alternative, shorter way is to find non-digits \D and remove them from the string:
Usually we pay little attention to spaces. For us strings 1-5 and 1 - 5 are nearly
identical.
But if a regexp doesn’t take spaces into account, it may fail to work.
alert( "1-5".match(/\d - \d/) ); // null, because the string 1-5 has no spaces
The dot "." is a special character class that matches “any character except a
newline”.
For instance:
alert( "Z".match(/./) ); // Z
Please note that the dot means “any character”, but not the “absense of a character”.
There must be a character to match it:
For instance, A.B matches A , and then B with any character between them,
except a newline.
This doesn’t match:
Summary
The Unicode encoding, used by JavaScript for strings, provides many properties for
characters, like: which language the letter belongs to (if a letter) it is it a punctuation
sign, etc.
Modern JavaScript allows to use these properties in regexps to look for characters,
for instance:
● A cyrillic letter is: \p{Script=Cyrillic} or \p{sc=Cyrillic} .
● A dash (be it a small hyphen - or a long dash — ): \p{Dash_Punctuation}
or \p{pd} .
● A currency symbol, such as $ , € or another: \p{Currency_Symbol} or
\p{sc} .
● …And much more. Unicode has a lot of character categories that we can select
from.
These patterns require 'u' regexp flag to work. More about that in the chapter
Unicode: flag "u".
✔ Tasks
The time has a format: hours:minutes . Both hours and minutes has two digits,
like 09:00 .
Make a regexp to find time in the string: Breakfast at 09:00 in the room
123:456.
P.S. In this task there’s no need to check time correctness yet, so 25:99 can also
be a valid result. P.P.S. The regexp shouldn’t match 123:456 .
To solution
Don’t try to remember the list – soon we’ll deal with each of them separately and
you’ll know them by heart automatically.
Escaping
Let’s say we want to find a dot literally. Not “any character”, but just a dot.
To use a special character as a regular one, prepend it with a backslash: \. .
For example:
If we’re looking for a backslash \ , it’s a special character in both regular strings and
regexps, so we should double it.
A slash
A slash symbol '/' is not a special character, but in JavaScript it is used to open
and close the regexp: /...pattern.../ , so we should escape it too.
On the other hand, if we’re not using /.../ , but create a regexp using new
RegExp , then we don’t need to escape it:
new RegExp
If we are creating a regular expression with new RegExp , then we don’t have to
escape / , but need to do some other escaping.
The search worked with /\d\.\d/ , but with new RegExp("\d\.\d") it doesn’t
work, why?
The reason is that backslashes are “consumed” by a string. Remember, regular
strings have their own special characters like \n , and a backslash is used for
escaping.
Please, take a look, what “\d.\d” really is:
alert("\d\.\d"); // d.d
So the call to new RegExp gets a string without backslashes. That’s why the
search doesn’t work!
Summary
● To search special characters [ \ ^ $ . | ? * + ( ) literally, we need to
prepend them with \ (“escape them”).
● We also need to escape / if we’re inside /.../ (but not inside new RegExp ).
● When passing a string new RegExp , we need to double backslashes \\ , cause
strings consume one of them.
Sets
For instance, [eao] means any of the 3 characters: 'a' , 'e' , or 'o' .
That’s called a set. Sets can be used in a regexp along with regular characters:
Please note that although there are multiple characters in the set, they correspond to
exactly one character in the match.
So the example below gives no matches:
Ranges
In the example below we’re searching for "x" followed by two digits or letters from
A to F :
Please note that in the word Exception there’s a substring xce . It didn’t match
the pattern, because the letters are lowercase, while in the set [0-9A-F] they are
uppercase.
If we want to find it too, then we can add a range a-f : [0-9A-Fa-f] . The i flag
would allow lowercase too.
For instance, we want to match all wordly characters or a dash, for words like
“twenty-third”. We can’t do it with \w+ , because \w class does not include a dash.
But we can use [\w-] .
We also can use several classes, for example [\s\S] matches spaces or non-
spaces – any character. That’s wider than a dot "." , because the dot matches any
character except a newline (unless s flag is set).
Excluding ranges
Besides normal ranges, there are “excluding” ranges that look like [^…] .
They are denoted by a caret character ^ at the start and match any character
except the given ones.
For instance:
● [^aeyo] – any character except 'a' , 'e' , 'y' or 'o' .
● [^0-9] – any character except a digit, the same as \D .
● [^\s] – any non-space character, same as \S .
The example below looks for any characters except letters, digits and spaces:
No escaping in […]
Usually when we want to find exactly the dot character, we need to escape it like
\. . And if we need a backslash, then we use \\ .
In square brackets the vast majority of special characters can be used without
escaping:
● A dot '.' .
● A plus '+' .
●
Parentheses '( )' .
●
Dash '-' in the beginning or the end (where it does not define a range).
● A caret '^' if not in the beginning (where it means exclusion).
● And the opening square bracket '[' .
In other words, all special characters are allowed except where they mean
something for square brackets.
A dot "." inside square brackets means just a dot. The pattern [.,] would look
for one of characters: either a dot or a comma.
In the example below the regexp [-().^+] looks for one of the characters -
().^+ :
// No need to escape
let reg = /[-().^+]/g;
…But if you decide to escape them “just in case”, then there would be no harm:
// Escaped everything
let reg = /[\-\(\)\.\^\+]/g;
✔ Tasks
Java[^script]
To solution
To solution
Quantity {n}
alert(numbers); // 7,903,123,45,67
Shorthands
+
Means “one or more”, the same as {1,} .
?
Means “zero or one”, the same as {0,1} . In other words, it makes the symbol
optional.
For instance, the pattern ou?r looks for o followed by zero or one u , and then r .
*
Means “zero or more”, the same as {0,} . That is, the character may repeat any
times or be absent.
For example, \d0* looks for a digit followed by any number of zeroes:
alert( "100 10 1".match(/\d0*/g) ); // 100, 10, 1
More examples
Quantifiers are used very often. They serve as the main “building block” of complex
regular expressions, so let’s see more examples.
We look for character '<' followed by one or more Latin letters, and then '>' .
For instance, for HTML tags we could use a simpler regexp: <\w+> .
…But because \w means any Latin letter or a digit or '_' , the regexp also
matches non-tags, for instance <_> . So it’s much simpler than <[a-z][a-z0-
9]*> , but less reliable.
In real life both variants are acceptable. Depends on how tolerant we can be to
“extra” matches and whether it’s difficult or not to filter them out by other means.
✔ Tasks
Check it:
To solution
An example of use:
To solution
The first thing to do is to locate quoted strings, and then we can replace them.
A regular expression like /".+"/g (a quote, then something, then the other quote)
may seem like a good fit, but it isn’t!
Greedy search
To find a match, the regular expression engine uses the following algorithm:
● For every position in the string
● Match the pattern at that position.
● If there’s no match, go to the next position.
These common words do not make it obvious why the regexp fails, so let’s elaborate
how the search works for the pattern ".+" .
The regular expression engine tries to find it at the zero position of the source
string a "witch" and her "broom" is one , but there’s a there, so
there’s immediately no match.
Then it advances: goes to the next positions in the source string and tries to find
the first character of the pattern there, and finally finds the quote at the 3rd
position:
2. The quote is detected, and then the engine tries to find a match for the rest of the
pattern. It tries to see if the rest of the subject string conforms to .+" .
In our case the next pattern character is . (a dot). It denotes “any character
except a newline”, so the next string letter 'w' fits:
3. Then the dot repeats because of the quantifier .+ . The regular expression
engine builds the match by taking characters one by one while it is possible.
…When does it become impossible? All characters match the dot, so it only stops
when it reaches the end of the string:
4. Now the engine finished repeating for .+ and tries to find the next character of
the pattern. It’s the quote " . But there’s a problem: the string has finished, there
are no more characters!
The regular expression engine understands that it took too many .+ and starts to
backtrack.
In other words, it shortens the match for the quantifier by one character:
Now it assumes that .+ ends one character before the end and tries to match the
rest of the pattern from that position.
If there were a quote there, then that would be the end, but the last character is
'e' , so there’s no match.
6. The engine keep backtracking: it decreases the count of repetition for '.' until
the rest of the pattern (in our case '"' ) matches:
In the greedy mode (by default) the quantifier is repeated as many times as
possible.
The regexp engine tries to fetch as many characters as it can by .+ , and then
shortens that one by one.
For our task we want another thing. That’s what the lazy quantifier mode is for.
Lazy mode
The lazy mode of quantifier is an opposite to the greedy mode. It means: “repeat
minimal number of times”.
We can enable it by putting a question mark '?' after the quantifier, so that it
becomes *? or +? or even ?? for '?' .
To clearly understand the change, let’s trace the search step by step.
1. The first step is the same: it finds the pattern start '"' at the 3rd position:
2. The next step is also similar: the engine finds a match for the dot '.' :
3. And now the search goes differently. Because we have a lazy mode for +? , the
engine doesn’t try to match a dot one more time, but stops and tries to match the
rest of the pattern '"' right now:
If there were a quote there, then the search would end, but there’s 'i' , so
there’s no match.
4. Then the regular expression engine increases the number of repetitions for the dot
and tries one more time:
Failure again. Then the number of repetitions is increased again and again…
6. The next search starts from the end of the current match and yield one more
result:
In this example we saw how the lazy mode works for +? . Quantifiers +? and ??
work the similar way – the regexp engine increases the number of repetitions only if
the rest of the pattern can’t match on the given position.
1. The pattern \d+ tries to match as many numbers as it can (greedy mode), so it
finds 123 and stops, because the next character is a space ' ' .
3. Then there’s \d+? . The quantifier is in lazy mode, so it finds one digit 4 and
tries to check if the rest of the pattern matches from there.
…But there’s nothing in the pattern after \d+? .
The lazy mode doesn’t repeat anything without a need. The pattern finished, so
we’re done. We have a match 123 4 .
Optimizations
Modern regular expression engines can optimize internal algorithms to work
faster. So they may work a bit different from the described algorithm.
Complex regular expressions are hard to optimize, so the search may work
exactly as described as well.
Alternative approach
With regexps, there’s often more than one way to do the same thing.
In our case we can find quoted strings without lazy mode using the regexp "
[^"]+" :
The regexp "[^"]+" gives correct results, because it looks for a quote '"'
followed by one or more non-quotes [^"] , and then the closing quote.
When the regexp engine looks for [^"]+ it stops the repetitions when it meets the
closing quote, and we’re done.
Please note, that this logic does not replace lazy quantifiers!
It is just different. There are times when we need one or another.
Let’s see an example where lazy quantifiers fail and this variant works right.
For instance, we want to find links of the form <a href="..." class="doc"> ,
with any href .
// Works!
alert( str.match(reg) ); // <a href="link" class="doc">
It worked. But let’s see what happens if there are many links in the text?
Now the result is wrong for the same reason as our “witches” example. The
quantifier .* took too many characters.
// Works!
alert( str.match(reg) ); // <a href="link1" class="doc">, <a href="link2" class="doc
// Wrong match!
alert( str.match(reg) ); // <a href="link1" class="wrong">... <p style="" class="doc
Now it fails. The match includes not just a link, but also a lot of text after it, including
<p...> .
Why?
But the problem is: that’s already beyond the link, in another tag <p> . Not what we
want.
Here’s the picture of the match aligned with the text:
The correct variant would be: href="[^"]*" . It will take all characters inside the
href attribute till the nearest quote, just what we need.
A working example:
// Works!
alert( str1.match(reg) ); // null, no matches, that's correct
alert( str2.match(reg) ); // <a href="link1" class="doc">, <a href="link2" class="do
Summary
Greedy
By default the regular expression engine tries to repeat the quantifier as many times
as possible. For instance, \d+ consumes all possible digits. When it becomes
impossible to consume more (no more digits or string end), then it continues to
match the rest of the pattern. If there’s no match then it decreases the number of
repetitions (backtracks) and tries again.
Lazy
Enabled by the question mark ? after the quantifier. The regexp engine tries to
match the rest of the pattern before each repetition of the quantifier.
As we’ve seen, the lazy mode is not a “panacea” from the greedy search. An
alternative is a “fine-tuned” greedy search, with exclusions. Soon we’ll see more
examples of it.
✔ Tasks
To solution
To solution
Find HTML tags
Create a regular expression to find all (opening and closing) HTML tags with their
attributes.
An example of use:
Here we assume that tag attributes may not contain < and > (inside squotes too),
that simplifies things a bit.
To solution
Capturing groups
A part of a pattern can be enclosed in parentheses (...) . This is called a
“capturing group”.
Example
In the example below the pattern (go)+ finds one or more 'go' :
1. The first part [-.\w]+ (before @ ) may include any alphanumeric word
characters, a dot and a dash, to match john.smith .
2. Then @ , and the domain. It may be a subdomain like host.site.com.uk , so
we match it as "a word followed by a dot ([\w-]+\.) (repeated), and then the
last part must be a word: com or uk (but not very long: 2-20 characters).
That regexp is not perfect, but good enough to fix errors or occasional mistypes.
In this example parentheses were used to make a group for repeating (...)+ . But
there are other uses too, let’s see them.
Contents of parentheses
Parentheses are numbered from left to right. The search engine remembers the
content matched by each of them and allows to reference it in the pattern or in the
replacement string.
For instance, we’d like to find HTML tags <.*?> , and process them.
Let’s wrap the inner content into parentheses, like this: <(.*?)> .
We’ll get both the tag as a whole and its content as an array:
The call to String#match returns groups only if the regexp only looks for the first
match, that is: has no /.../g flag.
If we need all matches with their groups then we can use .matchAll or
regexp.exec as described in Methods of RegExp and String:
let str = '<h1>Hello, world!</h1>';
Here we have two matches for <(.*?)> , each of them is an array with the full
match and groups.
Nested groups
Parentheses can be nested. In this case the numbering also goes from left to right.
Then groups, numbered from left to right. Whichever opens first gives the first group
result[1] . Here it encloses the whole tag content.
Then in result[2] goes the group from the second opening ( till the
corresponding ) – tag name, then we don’t group spaces, but group attributes for
result[3] .
For instance, let’s consider the regexp a(z)?(c)? . It looks for "a" optionally
followed by "z" optionally followed by "c" .
If we run it on the string with a single letter a , then the result is:
alert( match.length ); // 3
alert( match[0] ); // a (whole match)
alert( match[1] ); // undefined
alert( match[2] ); // undefined
The array has the length of 3 , but all groups are empty.
alert( match.length ); // 3
alert( match[0] ); // ac (whole match)
alert( match[1] ); // undefined, because there's nothing for (z)?
alert( match[2] ); // c
The array length is permanent: 3 . But there’s nothing for the group (z)? , so the
result is ["ac", undefined, "c"] .
Named groups
Remembering groups by their numbers is hard. For simple patterns it’s doable, but
for more complex ones we can give names to parentheses.
That’s done by putting ?<name> immediately after the opening paren, like this:
alert(groups.year); // 2019
alert(groups.month); // 04
alert(groups.day); // 30
As you can see, the groups reside in the .groups property of the match.
We can also use them in the replacement string, as $<name> (like $1..9 , but a
name instead of a digit).
alert(rearranged); // 30.04.2019
If we use a function for the replacement, then named groups object is always the
last argument:
alert(rearranged); // 30.04.2019
Usually, when we intend to use named groups, we don’t need positional arguments
of the function. For the majority of real-life cases we only need str and groups .
For instance, if we want to find (go)+ , but don’t want to remember the contents
( go ) in a separate array item, we can write: (?:go)+ .
In the example below we only get the name “John” as a separate member of the
results array:
alert( result.length ); // 2
alert( result[1] ); // John
Summary
Parentheses group together a part of the regular expression, so that the quantifier
applies to it as a whole.
Parentheses groups are numbered left-to-right, and can optionally be named with
(?<name>...) .
The content, matched by a group, can be referenced both in the replacement string
as $1 , $2 etc, or by the name $name if named.
So, parentheses groups are called “capturing groups”, as they “capture” a part of the
match. We get that part separately from the result as a member of the array or in
.groups if it’s named.
✔ Tasks
Write a RegExp that matches colors in the format #abc or #abcdef . That is: #
followed by 3 or 6 hexadecimal digits.
Usage example:
P.S. This should be exactly 3 or 6 hex digits: values like #abcd should not match.
To solution
Create a regexp that looks for positive numbers, including those without a decimal
point.
An example of use:
To solution
Write a regexp that looks for all decimal numbers including integer ones, with the
floating point and negative ones.
An example of use:
To solution
Parse an expression
An arithmetical expression consists of 2 numbers and an operator between them, for
instance:
● 1 + 2
● 1.2 * 3.4
● -3 / -6
● -2 - 2
There may be extra spaces at the beginning, at the end or between the parts.
For example:
alert(a); // 1.2
alert(op); // *
alert(b); // 3.4
To solution
Backreference by number: \n
A group can be referenced in the pattern using \n , where n is the group number.
As we can see, the pattern found an opening quote " , then the text is consumed
lazily till the other quote ' , that closes the match.
To make sure that the pattern looks for the closing quote exactly the same as the
opening one, we can wrap it into a capturing group and use the backreference.
Now it works! The regular expression engine finds the first quote (['"]) and
remembers the content of (...) , that’s the first capturing group.
Further in the pattern \1 means “find the same text as in the first group”, exactly the
same quote in our case.
Please note:
●
To reference a group inside a replacement string – we use $1 , while in the
pattern – a backslash \1 .
● If we use ?: in the group, then we can’t reference it. Groups that are excluded
from capturing (?:...) are not remembered by the engine.
Alternation (OR) |
Alternation is the term in regular expression that is actually a simple “OR”.
A usage example:
We already know a similar thing – square brackets. They allow to choose between
multiple character, for instance gr[ae]y matches gray or grey .
Square brackets allow only characters or character sets. Alternation allows any
expressions. A regexp A|B|C means one of expressions A , B or C .
For instance:
● gr(a|e)y means exactly the same as gr[ae]y .
●
gra|ey means gra or ey .
In previous chapters there was a task to build a regexp for searching time in the form
hh:mm , for instance 12:00 . But a simple \d\d:\d\d is too vague. It accepts
25:99 as the time (as 99 seconds match the pattern).
As a regexp: [01]\d|2[0-3] .
Next, the minutes must be from 0 to 59 . In the regexp language that means [0-
5]\d : the first digit 0-5 , and then any digit.
We’re almost done, but there’s a problem. The alternation | now happens to be
between [01]\d and 2[0-3]:[0-5]\d .
✔ Tasks
There are many programming languages, for instance Java, JavaScript, PHP, C,
C++.
Create a regexp that finds them in the string Java JavaScript PHP C++ C :
To solution
For instance:
[b]text[/b]
[url]http://google.com[/url]
BB-tags can be nested. But a tag can’t be nested into itself, for instance:
Normal:
[url] [b]http://google.com[/b] [/url]
[quote] [b]text[/b] [/quote]
Impossible:
[b][b]text[/b][/b]
[quote]
[b]text[/b]
[/quote]
For instance:
If tags are nested, then we need the outer tag (if we want we can continue the
search in its content):
To solution
The strings should support escaping, the same way as JavaScript strings do. For
instance, quotes can be inserted as \" a newline as \n , and the slash itself as
\\ .
let str = "Just like \"here\".";
Please note, in particular, that an escaped quote \" does not end a string.
So we should search from one quote to the other ignoring escaped quotes on the
way.
.. "test me" ..
.. "Say \"Hello\"!" ... (escaped quotes inside)
.. "\\" .. (double slash inside)
.. "\\ \"" .. (double slash and an escaped quote inside)
In JavaScript we need to double the slashes to pass them right into the string, like
this:
let str = ' .. "test me" .. "Say \\"Hello\\"!" .. "\\\\ \\"" .. ';
To solution
Write a regexp to find the tag <style...> . It should match the full tag: it may have
no attributes <style> or have several of them <style type="..."
id="..."> .
For instance:
To solution
String start ^ and finish $
The caret '^' and dollar '$' characters have special meaning in a regexp. They
are called “anchors”.
The caret ^ matches at the beginning of the text, and the dollar $ – in the end.
let str1 = "Mary had a little lamb, it's fleece was white as snow";
let str2 = 'Everywhere Mary went, the lamp was sure to go';
The pattern ^Mary means: “the string start and then Mary”.
To test whether the string ends with the email, let’s add $ to the pattern:
We can use both anchors together to check whether the string exactly follows the
pattern. That’s often used for validation.
For instance we want to check that str is exactly a color in the form # plus 6 hex
digits. The pattern for the color is #[0-9a-f]{6} .
To check that the whole string exactly matches it, we add ^...$ :
The regexp engine looks for the text start, then the color, and then immediately the
text end. Just what we need.
Anchors have zero length
Anchors just like \b are tests. They have zero-width.
In other words, they do not match a character, but rather force the regexp engine
to check the condition (text start/end).
The behavior of anchors changes if there’s a flag m (multiline mode). We’ll explore it
in the next chapter.
✔ Tasks
Regexp ^$
To solution
Check MAC-address
Usage:
To solution
In the multiline mode they match not only at the beginning and end of the string, but
also at start/end of line.
Line start ^
In the example below the text has multiple lines. The pattern /^\d+/gm takes a
number from the beginning of each one:
alert( str.match(/^\d+/gm) ); // 1, 2, 33
The regexp engine moves along the text and looks for a line start ^ , when finds –
continues to match the rest of the pattern \d+ .
alert( str.match(/^\d+/g) ); // 1
That’s because by default a caret ^ only matches at the beginning of the text, and in
the multiline mode – at the start of any line.
Line end $
The regular expression \w+$ finds the last word in every line
Anchors ^$ versus \n
To find a newline, we can use not only ^ and $ , but also the newline character \n .
The first difference is that unlike anchors, the character \n “consumes” the newline
character and adds it to the result.
So, anchors are usually better, they are closer to what we want to get.
We need a number (let’s say a price has no decimal point) followed by € sign.
Lookahead
The syntax is: x(?=y) , it means "look for x , but match only if followed by y ".
Lookbehind
Lookbehind is similar, but it looks behind. That is, it allows to match a pattern only if
there’s something before.
The syntax is:
●
Positive lookbehind: (?<=y)x , matches x , but only if it follows after y .
● Negative lookbehind: (?<!y)x , matches x , but only if there’s no y before.
For example, let’s change the price to US dollars. The dollar sign is usually before
the number, so to look for $30 we’ll use (?<=\$)\d+ – an amount preceded by
$:
And, to find the quantity – a number, not preceded by $ , we can use a negative
lookbehind (?<!\$)\d+ :
Capture groups
Generally, what’s inside the lookaround (a common name for both lookahead and
lookbehind) parentheses does not become a part of the match.
E.g. in the pattern \d+(?=€) , the € sign doesn’t get captured as a part of the
match. That’s natural: we look for a number \d+ , while (?=€) is just a test that it
should be followed by € .
But in some situations we might want to capture the lookaround expression as well,
or a part of it. That’s possible. Just wrap that into additional parentheses.
For instance, here the currency (€|kr) is captured, along with the amount:
Please note that for lookbehind the order stays be same, even though lookahead
parentheses are before the main pattern.
Summary
The typical situation – a regular expression works fine sometimes, but for certain
strings it “hangs” consuming 100% of CPU.
Introduction
We want to find all tags, with or without attributes – like <a href="..."
class="doc" ...> . We need the regexp to work reliably, because HTML comes
from the internet and can be messy.
In particular, we need it to match tags like <a test="<>" href="#"> – with <
and > in attributes. That’s allowed by HTML standard .
A simple regexp like <[^>]+> doesn’t work, because it stops at the first > , and we
need to ignore <> if inside an attribute:
If we substitute these into the pattern above and throw in some optional spaces \s ,
the full regexp becomes: <\w+(\s*\w+="[^"]*"\s*)*> .
That regexp is not perfect! It doesn’t support all the details of HTML syntax, such as
unquoted values, and there are other ways to improve, but let’s not add complexity. It
will demonstrate the problem for us.
The regexp seems to work:
Great! It found both the long tag <a test="<>" href="#"> and the short one
<b> .
Now, that we’ve got a seemingly working solution, let’s get to the infinite backtracking
itself.
Infinite backtracking
If you run our regexp on the input below, it may hang the browser (or another
JavaScript host):
let str = `<tag a="b" a="b" a="b" a="b" a="b" a="b" a="b" a="b"
a="b" a="b" a="b" a="b" a="b" a="b" a="b" a="b" a="b" a="b" a="b" a="b"
Some regexp engines can handle that search, but most of them can’t.
What’s the matter? Why a simple regular expression “hangs” on such a small string?
Let’s simplify the regexp by stripping the tag name and the quotes. So that we look
only for key=value attributes: <(\s*\w+=\w+\s*)*> .
let str = `<a=b a=b a=b a=b a=b a=b a=b a=b
a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b`;
Here we end the demo of the problem and start looking into what’s going on, why it
hangs and how to fix it.
Detailed example
This regular expression also has the same problem. In most regexp engines that
search takes a very long time (careful – can hang):
alert( '12345678901234567890123456789123456789z'.match(/(\d+)*$/) );
Indeed, the regexp is artificial. But the reason why it is slow is the same as those we
saw above. So let’s understand it, and then the previous example will become
obvious.
What happens during the search of (\d+)*$ in the line 123456789z ?
1. First, the regexp engine tries to find a number \d+ . The plus + is greedy by
default, so it consumes all digits:
\d+.......
(123456789)z
2. Then it tries to apply the star quantifier, but there are no more digits, so it the star
doesn’t give anything.
3. Then the pattern expects to see the string end $ , and in the text we have z , so
there’s no match:
X
\d+........$
(123456789)z
\d+.......
(12345678)9z
5. Now the engine tries to continue the search from the new position ( 9 ).
\d+.......\d+
(12345678)(9)z
X
\d+.......\d+
(12345678)(9)z
X
\d+......\d+
(1234567)(89)z
The search engine backtracks again. Backtracking generally works like this: the
last greedy quantifier decreases the number of repetitions until it can. Then the
previous greedy quantifier decreases, and so on. In our case the last greedy
quantifier is the second \d+ , from 89 to 8 , and then the star takes 9 :
X
\d+......\d+\d+
(1234567)(8)(9)z
7. …Fail again. The second and third \d+ backtracked to the end, so the first
quantifier shortens the match to 123456 , and the star takes the rest:
X
\d+.......\d+
(123456)(789)z
Again no match. The process repeats: the last greedy quantifier releases one
character ( 9 ):
X
\d+.....\d+ \d+
(123456)(78)(9)z
8. …And so on.
The regular expression engine goes through all combinations of 123456789 and
their subsequences. There are a lot of them, that’s why it takes so long.
What to do?
Should we turn on the lazy mode?
// sloooooowwwwww
alert( '12345678901234567890123456789123456789z'.match(/(\d+?)*$/) );
Some regular expression engines have tricky built-in checks to detect infinite
backtracking or other means to work around them, but there’s no universal solution.
Back to tags
How to fix?
The backtracking checks many variants that are an obvious fail for a human.
For instance, in the pattern (\d+)*$ a human can easily see that (\d+)* does
not need to backtrack + . There’s no difference between one or two \d+ :
\d+........
(123456789)z
\d+...\d+....
(1234)(56789)z
(name=value) name=value
Modern regexp engines support so-called “possessive” quantifiers for that. They are
like greedy, but don’t backtrack at all. Pretty simple, they capture whatever they can,
and the search continues. There’s also another tool called “atomic groups” that forbid
backtracking inside parentheses.
In other words:
●
The lookahead ?= looks for the maximal count a+ from the current position.
● And then they are “consumed into the result” by the backreference \1 ( \1
corresponds to the content of the second parentheses, that is a+ ).
There will be no backtracking, because lookahead does not backtrack. If, for
example, it found 5 instances of a+ and the further match failed, it won’t go back to
the 4th instance.
Please note:
There’s more about the relation between possessive quantifiers and lookahead
in articles Regex: Emulate Atomic Grouping (and Possessive Quantifiers) with
LookAhead and Mimicking Atomic Groups .
let badInput = `<tag a=b a=b a=b a=b a=b a=b a=b a=b
a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b`;
Great, it works! We found both a long tag <a test="<>" href="#"> and a
small one <b> , and (!) didn’t hang the engine on the bad input.
Let’s briefly review them here. In short, normally characters are encoded with 2
bytes. That gives us 65536 characters maximum. But there are more characters in
the world.
a 0x0061 2
≈ 0x2248 2
𝒳 0x1d4b3 4
𝒴 0x1d4b4 4
😄 0x1f604 4
So characters like a and ≈ occupy 2 bytes, and those rare ones take 4.
The unicode is made in such a way that the 4-byte characters only have a meaning
as a whole.
In the past JavaScript did not know about that, and many string methods still have
problems. For instance, length thinks that here are two characters:
alert('😄'.length); // 2
alert('𝒳'.length); // 2
…But we can see that there’s only one, right? The point is that length treats 4
bytes as two 2-byte characters. That’s incorrect, because they must be considered
only together (so-called “surrogate pair”).
Normally, regular expressions also treat “long characters” as two 2-byte ones.
That leads to odd results, for instance let’s try to find [𝒳𝒴] in the string 𝒳 :
alert( '𝒳'.match(/[𝒳𝒴]/u) ); // 𝒳
alert( '𝒴'.match(/[𝒳-𝒵]/u) ); // 𝒴
In regular expressions these can be set by \p{…} . And there must be flag 'u' .
For instance, \p{Letter} denotes a letter in any of language. We can also use
\p{L} , as L is an alias of Letter , there are shorter aliases for almost every
property.
Here’s the main tree of properties:
●
Letter L :
● lowercase Ll , modifier Lm , titlecase Lt , uppercase Lu , other Lo
● Number N :
● decimal digit Nd , letter number Nl , other No
●
Punctuation P :
● connector Pc , dash Pd , initial quote Pi , final quote Pf , open Ps , close
Pe , other Po
● Mark M (accents etc):
● spacing combining Mc , enclosing Me , non-spacing Mn
● Symbol S :
● currency Sc , modifier Sk , math Sm , other So
● Separator Z :
●
line Zl , paragraph Zp , space Zs
● Other C :
● control Cc , format Cf , not assigned Cn , private use Co , surrogate Cs
More information
Interested to see which characters belong to a property? There’s a tool at
http://cldr.unicode.org/unicode-utilities/list-unicodeset for that.
For the full Unicode Character Database in text format (along with all properties),
see https://www.unicode.org/Public/UCD/latest/ucd/ .
There are also properties with a value. For instance, Unicode “Script” (a writing
system) can be Cyrillic, Greek, Arabic, Han (Chinese) etc, the list is long.
To search for characters in certain scripts (“alphabets”), we should supply Script=
<value> , e.g. to search for cyrillic letters: \p{sc=Cyrillic} , for Chinese
glyphs: \p{sc=Han} , etc:
alert( str.match(regexp) ); // 你好
Building multi-language \w
The pattern \w means “wordly characters”, but doesn’t work for languages that use
non-Latin alphabets, such as Cyrillic and others. It’s just a shorthand for [a-zA-
Z0-9_] , so \w+ won’t find any Chinese words etc.
Let’s make a “universal” regexp, that looks for wordly characters in any language.
That’s easy to do using Unicode properties:
/[\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]
The match is found, because regexp.exec starts to search from the given
position and goes on by the text, successfully matching “function” later.
So we’ve came to the problem: how to search for a match exactly at the given
position.
That’s what y flag does. It makes the regexp search only at the lastIndex
position.
Here’s an example
As we can see, now the regexp is only matched at the given position.
So what y does is truly unique, and very important for writing parsers.
The y flag allows to test a regular expression exactly at the given position and when
we understand what’s there, we can move on – step by step examining the text.
Without the flag the regexp engine always searches till the end of the text, that takes
time, especially if the text is large. So our parser would be very slow. The y flag is
exactly the right thing here.
Solutions
ArrayBuffer, binary arrays
function concat(arrays) {
// sum of individual array lengths
let totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
return result;
}
To formulation
Fetch
Fetch users from GitHub
1. fetch('https://api.github.com/users/USERNAME') .
2. If the response has status 200 , call .json() to read the JS object.
If a fetch fails, or the response has non-200 status, we just return null
in the resulting arrray.
return results;
}
To formulation
As we’ll see, fetch also has options that prevent sending the Referer
and even allow to change it (within the same site).
To formulation
LocalStorage, sessionStorage
To formulation
CSS-animations
Animate a plane (CSS)
/* original class */
#flyjet {
transition: all 3s;
}
/* JS adds .growing */
#flyjet.growing {
width: 400px;
height: 240px;
}
Please note that transitionend triggers two times – once for every
property. So if we don’t perform an additional check then the message would
show up 2 times.
To formulation
We need to choose the right Bezier curve for that animation. It should have
y>1 somewhere for the plane to “jump out”.
For instance, we can take both control points with y>1 , like: cubic-
bezier(0.25, 1.5, 0.75, 1.5) .
The graph:
Animated circle
To formulation
JavaScript animations
To to get the “bouncing” effect we can use the timing function bounce in
easeOut mode.
animate({
duration: 2000,
timing: makeEaseOut(bounce),
draw(progress) {
ball.style.top = to * progress + 'px'
}
});
To formulation
The horizontal coordinate changes by another law: it does not “bounce”, but
gradually increases shifting the ball to the right.
The code:
To formulation
Custom elements
Please note:
1. We clear setInterval timer when the element is removed from the
document. That’s important, otherwise it continues ticking even if not
needed any more. And the browser can’t clear the memory from this
element and referenced by it.
2. We can access current date as elem.date property. All class
methods and properties are naturally element methods and properties.
To formulation
Character classes
To formulation
Java[^script]
To formulation
Answer: \d\d[-:]\d\d .
Please note that the dash '-' has a special meaning in square brackets,
but only between other characters, not when it’s in the beginning or at the
end, so we don’t need to escape it.
To formulation
Solution:
Please note that the dot is a special character, so we have to escape it and
insert as \. .
To formulation
// color
alert( "#123456".match( /#[a-f0-9]{6}\b/gi ) ); // #123456
// not a color
alert( "#12345678".match( /#[a-f0-9]{6}\b/gi ) ); // null
To formulation
First the lazy \d+? tries to take as little digits as it can, but it has to reach
the space, so it takes 123 .
Then the second \d+? takes only one digit, because that’s enough.
To formulation
We need to find the beginning of the comment <!-- , then everything till the
end of --> .
The first idea could be <!--.*?--> – the lazy quantifier makes the dot
stop right before --> .
But a dot in JavaScript means “any symbol except the newline”. So multiline
comments won’t be found.
To formulation
To formulation
Capturing groups
We can add exactly 3 more optional hex digits. We don’t need more or less.
Either we have them or we don’t.
In action:
To formulation
An non-negative integer number is \d+ . A zero 0 can’t be the first digit, but
we should allow it in further digits.
Because the decimal part is optional, let’s put it in parentheses with the
quantifier ? .
A positive number with an optional decimal part is (per previous task): \d+
(\.\d+)? .
To formulation
Parse an expression
An operator is [-+*/] .
Please note:
● Here the dash - goes first in the brackets, because in the middle it would
mean a character range, while we just want a character - .
● A slash / should be escaped inside a JavaScript regexp /.../ , we’ll do
that later.
To get a result as an array let’s put parentheses around the data that we
need: numbers and the operator: (-?\d+(\.\d+)?)\s*([-+*/])\s*
(-?\d+(\.\d+)?) .
In action:
let reg = /(-?\d+(\.\d+)?)\s*([-+*\/])\s*(-?\d+(\.\d+)?)/;
We only want the numbers and the operator, without the full match or the
decimal parts.
The full match (the arrays first item) can be removed by shifting the array
result.shift() .
function parse(expr) {
let reg = /(-?\d+(?:\.\d+)?)\s*([-+*\/])\s*(-?\d+(?:\.\d+)?)/;
return result;
}
To formulation
Alternation (OR) |
The regular expression engine looks for alternations one-by-one. That is: first
it checks if we have Java , otherwise – looks for JavaScript and so on.
In action:
To formulation
In action:
let str = `
[b]hello![/b]
[quote]
[url]http://google.com[/url]
[/quote]
`;
Please note that we had to escape a slash for the closing tag [/\1] ,
because normally the slash closes the pattern.
To formulation
Step by step:
In action:
We need either a space after <style and then optionally something else or
the ending > .
In action:
To formulation
Regexp ^$
The empty string is the only match: it starts and immediately finishes.
The task once again demonstrates that anchors are not characters, but tests.
The string is empty "" . The engine first matches the ^ (input start), yes it’s
there, and then immediately the end $ , it’s here too. So there’s a match.
To formulation
Check MAC-address
We need that number NN , and then :NN repeated 5 times (more numbers);
The regexp is: [0-9a-f]{2}(:[0-9a-f]{2}){5}
Now let’s show that the match should capture all the text: start at the
beginning and end at the end. That’s done by wrapping the pattern in
^...$ .
Finally:
To formulation