JavaScript key concepts & syntax
JavaScript like all programming languages has certain concepts and syntax you need to understand in order to become proficient in its overall use. In this chapter, you'll learn key concepts & syntax that apply equally across any JavaScript application -- whether it's designed for user interfaces on browsers or server side business logic -- as well as key concepts & syntax that crosscut into other functional JavaScript topics, such as JavaScript data types, JavaScript object-orientated and prototype-based programming, JavaScript for loops and JavaScript asynchronous behavior.
Running JavaScript
There are two broad ways to run JavaScript. One option is to use an interactive environment (a.k.a. REPL Read–eval–print loop) -- which in itself is backed by a JavaScript engine -- to run JavaScript in a piecemeal fashion. While the other more typical option is to place JavaScript in .js
files and run these files through a JavaScript engine.
In both cases, a JavaScript engine is provided by either an application like a browser or a non-graphical user interface(GUI) piece of software.
JavaScript REPLs
There are two types of JavaScript REPLs. There are online JavaScript REPLs, like those accompanying the examples in this book, which allow you to run JavaScript in a few clicks relying on a third party service like https://babeljs.io/repl/ or https://replit.com/languages/javascript. And there are offline JavaScript REPLs that allow you to run JavaScript on a non-networked computer, like those included in the Google Chrome browser and Firefox browser or those provided by non-GUI software such as Node JS and Deno.
For example, to access the JavaScript REPL on a Google Chrome browser, go to the 'More Tools' menu -> 'Developer Tools' option or use the Ctrl-Shift-I keyboard combination, at which time a browser pane is open at the bottom, if you select the 'Console' tab on this last pane you can interactively type in JavaScript as shown in figure 3-1. To access the JavaScript REPL on a Firefox browser, go to the 'More Tools' menu -> 'Web Developer Tools' option or use the Ctrl-Shift-I keyboard combination, at which time a browser pane is open at the bottom, if you select the 'Console' tab on this last pane you can interactively type in JavaScript as shown in figure 3-2.
Figure 3-1. Google Chrome browser REPL console
Figure 3-2. Firefox browser REPL console
Unlike browsers, non-GUI software that can also operate as an offline JavaScript REPL requires an installation process. You can consult the Node JS installation process and Deno installation process in other chapters of the book. Once you install one of these non-GUI softwares, it's as simple as invoking its main executable (e.g. node
or deno
) to enter a JavaScript REPL environment, like it's shown in the Node JS REPL section.
The HTML <script>
element
Moving beyond REPLs, you'll rely heavily on the HTML <script>
element which is what browsers use to run JavaScript. For non-GUI software -- like Node JS and Deno -- you'll have no need for the <script>
element, since these environments don't have built-in HTML support like browsers. For non-GUI software you can instead place JavaScript in .js
files and run it directly using the software's executable (e.g. node myscript.js
).
The HTML <script>
element at its simplest can run JavaScript on browsers by placing it in between an opening <script>
tag and closing </script>
tag, following the same conventions as other HTML elements with opening and closing tags to delimit content (e.g. <p>...</p>
, <li>...</li>
). Listing 3-1 shows some JavaScript declared in a <script>
element.
Listing 3-1. JavaScript declared in <script>
element
<html> <head> <script> var x = 1; var y = 2; var z = x + y; alert(z); </script> </head> <body> <h1> An alert box triggered by a <script> element <h1> </body> </html>
See the Pen HTML <script> element with inline JavaScript - Modern JS by D Rubio (@modernjs) on CodePen.
If you run the example in listing 3-1, you'll see an alert box display the number three. The JavaScript logic in listing 3-1 first declares the x
and y
variables and assigns them a number, followed by declaring the z
variable that calculates the sum of x
and y
, finishing with the alert(z)
statement that creates an alert box with the value of z
.
Although inserting inline JavaScript in an HTML <script>
element is simple, it can become unwieldy over time. Another alternative is to place JavaScript in .js
files and reference them using a url path using the same HTML <script>
element, but adding the src
attribute with a value that points to a url with a .js
file.
In addition to supporting the src
attribute, the HTML <script>
element also supports multiple attributes that influence JavaScript definitions, processing and security behaviors. Table 3-1 illustrates the various HTML <script>
element attributes.
Table 3-1. HTML <script> element attributes
Purpose group | Attribute | Description | Value options |
---|---|---|---|
Definition | nomodule | Indicates if a <script> element should be ignored by browsers that support ES6 (ES2015) modules. Note a <script nomodule> element is often complemented by a <script type="module"> element. |
|
src | Defines the .js file to load |
| |
type | Defines the language or format of the data in the <script> element as a MIME type |
| |
Processing | async | Defines parallel downloading of .js files, allowing other activities (e.g. HTML processing, other <script> element) to proceed without waiting. Although the downloading of .js files is done in parallel, the execution of a .js file starts immediately after it's downloaded and blocks other activities like HTML processing. In addition, if two or more <script> elements use async , there's no guarantee they will run in their declared order since downloads are asynchronous and can vary depending on network latency or other factors. |
|
defer | Defines parallel downloading of .js files, allowing other activities (e.g. HTML processing, other <script> element) to proceed without waiting. The execution of all .js files marked with defer only starts once the HTML document has been completely processed -- technically the DOMContentLoaded event is fired -- guaranteeing access to elements in the HTML document. In addition, the execution of <script> elements with defer is guaranteed to be made in their declared order. |
| |
Security | crossorigin | Allows a <script> to influence its Cross-Origin Resource Sharing (CORS) request. |
|
integrity | Based on the Subresource Integrity specification[1], it defines a hash to verify the contents of a .js file haven't been tampered with. It's generally used on .js files hosted by third party sites. |
sha256 , sha384 or sha512 -- followed by a dash and the hash itself (e.g. sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4= ).
| |
nonce | A "nonce" (a.k.a. number only used once) to whitelist scripts via the HTTP header Content-Security-Policy in the script-src directive. |
| |
referrerpolicy | Defines a value to influence the value of the HTTP header Referer that accompanies a request for a .js file. |
|
To add more context to the HTML <script>
element attributes in table 3-1, the following sub-sections describe attributes in greater detail based on their purpose.
Definition attributes for the HTML <script>
element: nomodule
, src
& type
The JavaScript in listing 3-1 -- the four lines inbetween the <script>
tag & </script>
tag -- can be placed in a .js
file and loaded into an HTML file using the src
attribute. Assuming you place the JavaScript in a file named intro.js
and make it available under a web site's absolute path /assets/scripts/intro.js
, you can substitute the <script>
element in listing 3-1 with the element <script src="/assets/scripts/intro.js"></script>
to obtain the same results.
Whether you opt to use .js
files with the src
attribute or use inline JavaScript like listing 3-1, the <script>
element always implies a type="text/javascript"
attribute, indicating a MIME type[3] for the JavaScript language. Throughout the evolution of JavaScript and the HTML <script>
element, there were attempts to incorporate other JavaScript MIME types (e.g. application/javascript
, application/ecmascript
, text/ecmascript
), however, these MIME types are no longer relevant and should be considered legacy types. The only <script>
element type
attribute values you should use are: text/javascript
or module
.
A <script>
element in the form <script type="module" src="..."></script>
tells a browser the referenced JavaScript should be treated as a JavaScript ES6 (ES2015) module. As outlined in the modules, namespaces & module types section in the modern JavaScript essentials chapter, modules are one of the more convoluted topics in JavaScript. Later in this chapter, I'll explain the finer details of what constitutes a JavaScript ES6 (ES2015) module, but for the moment, it's sufficient you know the <script>
element with the type="module"
attribute is how browsers tell the difference between a file being a JavaScript ES6 (ES2015) module and a file containing plain JavaScript.
The <script>
element also supports the nomodule
attribute to tell a browser to ignore the <script>
element in case it supports JavaScript ES6 (ES2015) modules. The purpose of the nomodule
attribute is to allow older browsers -- that don't support JavaScript ES6 (ES2015) modules -- the ability to run a fallback <script>
element with equivalent JavaScript that isn't an ES6 (ES2015) module. In most cases, an element like <script nomodule src="pseudo-module-pre-es6.js">
is accompanied by an element like <script type="module" src="module-es6.js"></script>
, to deliver the following behavior: if a browser supports ES6 (2015) modules, it ignores the pseudo-module-pre-es6.js
file on account of the nomodule
attribute and loads the module-es6.js
file as a module on account of the type="module"
attribute; if a browser doesn't support ES6 (2015) modules, it loads the pseudo-module-pre-es6.js
file -- which contains the equivalent module functionality of module-es6.js
in plain JavaScript -- because it doesn't know what the nomodule
attribute does, plus it ignores the module-es6.js
file since it also doesn't know how to process type="module"
which is also an ES6 (2015) specific attribute.
Processing attributes for the HTML <script>
element: async
& defer
An HTML document can contain one or dozens of <script>
elements. The issue with having too many <script>
elements is you need to be aware of their processing behavior. When you declare a <script>
element with either inline JavaScript like listing 3-1 or reference a JavaScript .js
file with the src
attribute, the processing behavior is sychronous (a.k.a. blocking), which means nothing besides the processing of the <script>
element happens.
Although this default synchronous behavior creates a predicatable outcome, with the first declared <script>
element processed first, followed by the second <script>
element processed second and so on, this blocking behavior of not being able to do tasks in parallel can lead to a bad user experience. This potential bad user experience is rooted in the fact that users are primarily interested in seeing the HTML document -- which must also be processed -- and not on waiting for <script>
elements to be processed. But this is where you'll face a conundrum, most <script>
elements exist to alter or influence an HTML document, so how do you go about presenting an HTML document in the quickest way possible, without <script>
elements blocking the process ?
When it comes to presenting an HTML document in the quickest way possible, the position of HTML <script>
elements matters. Although you can technically place <script>
elements anywhere on an HTML document, there are implications to every position. The following is a list of the <script>
element positions and their implications:
- Randomly throughout an HTML document's
<body>
&</body>
tags.- Pro: Allows JavaScript logic to be inserted directly where it's required in an HTML document.
- Con: It blocks HTML processing at whatever random location the element is placed, plus it creates difficult content management since JavaScript logic in
<script>
elements is interspersed with regular presentation HTML elements.
- In between an HTML document's
<head>
&</head>
tags.- Pro: Allows the JavaScript logic to be quickly applied to any HTML element since it's loaded at the start of the HTML document.
- Con: It slows down the processing of HTML the most, since the JavaScript contents of the
<script>
elements must be processed first.
- Before an HTML document's closing
</body>
tag.- Pro: Improves HTML processing the most, since the JavaScript logic is processed once the HTML processing is complete.
- Con: It slows down the application of JavaScript to HTML elements the most, since it's done at the end of the HTML document.
An important and subtle behavior of the <script>
element which may not be obvious in this last list, is that <script>
elements with a src
attribute pointing to a .js
file must also undergo a download step as part of the processing workflow. Since download times can vary widely, depending on file sizes and network conditions, the <script>
element supports the async
and defer
attributes to influence download & processing behaviors.
Using either the async
or defer
attribute on a <script>
element with a src
attribute, allows the download process of a referenced .js
file to run in parallel, that is, as soon as it's encountered the download process begins and work can move forward to another <script>
element or the processing of the HTML document.
The only difference between using the async
and defer
attributes is when the actual contents of the JavaScript .js
file are processed. For <script>
elements with async
the contents are processed immediately after download, in the process blocking any other task, whereas for <script>
elements with defer
the contents are executed until the HTML document has been completely processed, specifically the DOMContentLoaded event is fired.
Tip The DOMContentLoaded event is the same one used by jQuery's$(document).ready()
method presented in the jQuery example in listing 1-2. Therefore, JavaScript wrapped in jQuery's$(document).ready()
method is equivalent to placing it inside a<script defer>
element.
Figure 3-3 illustrates how an HTML document is processed with a plain <script>
element, with a <script defer>
element, with a <script async>
element, as well as how <script>
loading behaves with a JavaScript module (i.e. <script type="module">
).
Figure 3-3. <script>
tag behavior with async
& defer
attributes; source HTML <script>
spec[4]
As you can see in figure 3-3, the <script defer>
element preemptively downloads JavaScript while HTML processing can continue, waiting until the end of HTML processing to process the JavaScript downloaded with defer
. The <script async>
element also preemptively downloads JavaScript while HTML processing can continue, but it interrupts HTML processing as soon as the download is complete to process the JavaScript with async
. For JavaScript modules, you can also see in figure 3-3 it isn't necessary to use the defer
attribute, since by default a <script type="module">
element is preemptively downloaded and processed until HTML processing is done. Finally, figure 3-3 also shows you can use the async
attribute on a JavaScript module to trigger the processing of a module while the HTML is still being processed.
Although preemptively downloading JavaScript using either defer
or async
on <script>
elements sounds like a great idea, both attributes can present undesired side effects.
One issue with <script>
elements that use async
is their processing order isn't necessarily the same as their declared order. Although all download processes start in the <script>
declared order, the download completions won't necessarily finish in the same order due to file size, network issues or other factors, making the processing order inconsistent. Therefore, if you need to guarantee a given exeuction order for <script>
elements (e.g. one <script>
element depends on another) you shouldn't use the async
attribute.
Unlike the async
attribute, <script>
elements that use defer
are guranteed to execute in their declared order. However, the reason they can guarantee the same execution order as their declared order, is because the execution is deferred until the HTML document is completely processed. Therefore, if you need certain JavaScript logic to be executed prior or as part of the HTML processing workflow, you shouldn't use the defer
attribute.
So plain <script>
elements, <script defer>
elements and <script async>
elements, all have their use. One technique I recommend you follow to determine when to use each one is to follow above the fold web design[5]. The 'above the fold' concept is derived from newspapers where readers can immediately see what's above the fold, but need to flip the newspaper to see other content. In web design, 'above the fold' refers to what users are able to see on their browsers before needing to scroll to see other content. Given the attention span of most users surfing the web, keeping the 'above the fold' loading times low for HTML documents is critical.
Applied to the placement of <script>
elements, 'above the fold' design would mean following these rules of thumb:
- Use plain
<script>
elements in the HTML<head>
section for JavaScript needed by 'above the fold' HTML content, minor blocking is justified to deliver better user experience. - Use the
defer
orasync
attributes on<script>
elements that aren't required 'above the fold', since preemptively downloading files is a plus in such cases. - Use the
async
attribute on<script>
elements that don't have dependencies and are secondary to the HTML document (e.g. advertisements, analytics tracking). - Use the
defer
attribute on<script>
elements that have dependencies with other<script>
elements and can be processed until after the HTML is processed.
Security attributes for the HTML <script>
element: crossorigin
, integrity
, nonce
& referrerpolicy
The web has become an increasingly hostile place as more commercial activity takes place on it. With JavaScript driving much of this activity on browsers, it should come as no surprise that the <script>
element has more attributes related to security than anything else.
The <script>
element's integrity
attribute allows browsers to validate if JavaScript files have been tampered with. All files have a unique hash -- or in layman's terms a fingerprint -- that when a file is modified by even a character or space, the file's hash changes. The integrity
attribute is widely used in JavaScript files distributed via content delivery networks (CDNs), where files can end up being handled and distributed by multiple parties that can alter their original source. To solve this potential tampering issue, the original creator of a file publishes its hash which is added to a <script>
element's integrity
value, ensuring that wherever a file is obtained it's a trustworthy copy.
The integrity
attribute validation workflow is done per the Subresource Integrity specification[1] supported by major browsers. In case a browser detects a <script>
element's integrity
attribute value doesn't match the value calculated by the browser, it blocks loading the JavaScript and generates an error in the form: "Failed to find a valid digest in the 'integrity' attribute for resource '<src_url>' with computed SHA-<hash_algorithm> integrity '<hash>'. The resource has been blocked"
, which is presented in a browser's console (figure 3-1 and figure 3-2).
The integrity
attribute supports three hash algorithms: she256
, sha384
and sha512
. The integrity
attribute value must be prefixed with the hash algorithm, followed by a dash and the hash itself (e.g. sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=
). If you want to create integrity
values to your <script>
elements you can use command line tools like shasum and openssl or an online service like https://www.srihash.org/.
The nonce
attribute allows <script>
elements with inline JavaScript to be whitelisted for loading into browsers. The issue with inline JavaScript is it can be subject to cross-site scripting (XSS), a security vulnerability where malicious users can either alter the original JavaScript or inject new JavaScript to execute rogue logic, hence the need to whitelist this kind of JavaScript via a "nonce" (a.k.a. number only used once).
In order to use the <script>
element's nonce
attribute it must be used in conjunction with Content Security Policy (CSP)[6], a more extensive XSS initiative, intended to tell browsers what they can safely load from the perspective of an HTML document owner. All nonce
values must be different for all requests made to an HTML document, therefore, it's imperative that nonce
values be generated by the server-side language dispatching the HTML document. In accordance with the CSP specification, a nonce
value must be at least 128 bits long and be generated by a secure random number generator, for which you can use a variety of libraries depending on the server-side programming language.
A nonce
value (e.g. MDg3slR6BAJ21zVYI7N4tw==
) should be added to all <script>
elements with inline JavaScript (e.g. <script nonce="MDg3slR6BAJ21zVYI7N4tw==">
). In addition, this same nonce
value must be added as part of the HTML document response, either as part of the HTTP header Content-Security-Policy
(e.g. Content-Security-Policy: script-src 'nonce-MDg3slR6BAJ21zVYI7N4tw=='
), or if you're not able to add the CSP HTTP header, a <meta>
element added to an HTML document's <head>
section (e.g. <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-MDg3slR6BAJ21zVYI7N4tw=='">
). In this manner, when a browser detects CSP as part of the HTML document response and notices a script-src
with a nonce-
value, it checks that <script>
elements with inline JavaScript match the nonce
value in CSP, if the nonce
values don't match the <script>
is blocked from loading.
Note CSP is a broad subject to prevent all kinds of XSS attacks, therefore the CSP HTTP header value or CSP<meta>
element value can be much more complex than thenonce
value just described. For example, a CSP value could also bescript-src 'self' ajax.googleapis.com;
to tell browsers to only allow loading of<script>
elements withsrc
files from the site origin (i.e. where the HTML document is hosted) or theajax.googleapis.com
domain. In the end, CSP is much more than allowing the loading of inline JavaScript or JavaScript files, it's about whitelisting the loading of any resource (e.g. images, media, frames) that can have XSS consequences.
The crossorigin
attribute allows <script>
elements to influence a browser's default Cross-Origin Resource Sharing (CORS) behavior. By default, all browsers have a same origin policy to execute JavaScript, which means, as long as all JavaScript interactions take place on the same origin -- url scheme (e.g. https://), site & port -- they're allowed with no oversight. So if a <script>
element originates on https://modernjs.com/
, the JavaScript logic can interact with any other end point on https://modernjs.com/
with no special requirements for the browser or site, however, if you attempt to make requests to another origin like https://api.modernjs.com
(different sub-domain) or https://en.wikipedia.org/w/api.php
then you'll face a CORS issue.
In practice, CORS works at the HTTP header level. If a request is made from JavaScript it's accompanied by the Origin
HTTP header (e.g. Origin: https://modernjs.com
). If the destination is the same as the origin, it's the same origin policy and works as expected. However, if the destination is another origin, the destination has a decision to make, does it allow the request from this other origin to go through ? The response from the destination come through in an another HTTP header Access-Control-Allow-Origin
(e.g. Access-Control-Allow-Origin: *
telling the requester all origins are allowed; or Access-Control-Allow-Origin: https://modernjs.com
telling the requester only the https://modernjs.com
origin is allowed). There are more subtleties to this whole process, like dry-runs to first verify access (a.k.a.'preflighted requests'), more specialized HTTP request headers (e.g. Access-Control-Request-Method
, Access-Control-Request-Headers
) and more specialized HTTP response headers (e.g. Access-Control-Allow-Methods
, Access-Control-Allow-Headers
), but the gist of CORS is an origin must be granted permissions by a destination if it's not the same origin.
Therefore a key factor to CORS workflows is destinations must grant permissions to different origins, something that can be a hassle if destinations need to grant access to dozens or hundreds of origins via the HTTP header Access-Control-Allow-Origin
. An easier way to grant these permissions is through credentials, in this manner a destination site can define a set of credentials (e.g. cookie, Basic Auth), that a browser can store for a given destination and send such credentials as part of the CORS request. The only thing that changes from the prior basic CORS workflow, is the requester must add the credentials for the destination and the destination must respond with the HTTP header Access-Control-Allow-Credentials: true
.
Armed with all this CORS context we can get back to the purpose of the <script>
's crossorigin
attribute. By default, the <script>
's crossorigin
attribute is set to anonymous
, which means requests made to destinations that are not the origin (i.e. cross-origin) be made anonomyously. However, another value for a <script>
's crossorigin
attribute is use-credentials
, which tells a browser that requests made to destinations that are not the origin be accompanied with any available credentials, streamlining access to cross-origin resources.
Finally, the referrerpolicy
attribute allows <script>
elements to influence a browser's HTTP header Referer
sent on HTML document requests. When users move from one HTML document to another, browsers leave a trial of breadcrumbs through the HTTP header Referer
indicating what url brought them to a given HTML document. Although the HTTP header Referer
can be a great source of information for analytics (e.g. user navigation workflows, third party site referrals), it can also be a source of private information leaks given the amount of data urls can contain (e.g. /profile/user/566056
, /user?email=john@gmail.com
, /profile/reset/password/sdsfA245G6
).
In order to control what type of data gets submitted via the HTTP header Referer
you can use a referrer policy. Referrer policies have their own standard[2] which supports eight different values with varying degrees of exposure, from the strictest level of not sending any data in the HTTP header Referer
with the no-referrer
referrer policy, to the laxest level of sending the referring url verbatim in the HTTP header Referer
with the unsafe-url
referrer policy.
All browsers have a default referrer policy which is used if no referrer policy is specified by an HTML document. For example, the Google Chrome browser defaults to the referrer policy strict-origin-when-cross-origin
, which indicates that the HTTP header Referer
value should only include the origin, path and query string for same origin requests, but for cross-origin requests the HTTP header Referer
value should only include the origin if the protocol security level is the same (i.e. https to https) and avoid sending any value altogether for cross-origin requests that are less secure destinations (i.e. from https to http).
Although a default referrer policy strict-origin-when-cross-origin
is a reasonable default, it's also possible to define a referrer policy on a per HTML document basis, by either using the HTTP header Referrer-Policy
in an HTML document response (e.g. Referrer-Policy: no-referrer
) or adding a <meta>
element to an HTML document's <head>
section (e.g. <meta name="referrer" content="no-referrer">
). Note the <meta>
element to add a referrer policy uses the name="referrer"
attribute with the content
attribute, since the <meta>
element to perform this via HTTP headers with http-equiv="Referrer-Policy"
is not supported by some browsers
So the purpose of the script
element's referrerpolicy
attribute is to define an explicit referrer policy for requests applicable to a single <script>
element. For example, you can use a definition like <script referrerpolicy="no-referrer" src="...">
so when a browser fetches the .js
file from src
there's no data sent in the HTTP header Referer
. This in turn provides very granular control of the referrer policy applied to <script>
requests, otherwise the referrer policy is determined from either the definition made on an HTML document or a user's browser.
The HTML <noscript>
element
In spite of all the effort you can put toward creating JavaScript and defining the appropriate <script>
elements with their corresponding attributes, there's always a possibility an end user can disable this functionality. This can be for various reasons, including: very security conscious users that don't want JavaScript to automatically execute on their devices; overly private users that disable advertisements for fear of tracking and with it also disable JavaScript; to users running browsers that don't support JavaScript.
The HTML <noscript>
element is the standard way to tell users they need to take certain actions on account of them disabling JavaScript. The HTML <noscript>
element is designed to wrap HTML elements and content to be displayed when a user disables JavaScript. Depending on the placement of the <noscript>
element in an HTML document its HTML elements can vary. If the <noscript>
element is placed in the HTML <head>
section of document it can only contain <link>
, <style>
or <meta>
elements, whereas if the <noscript>
element is placed in the HTML <body>
section of document it can contain a wider array of HTML elements for content (e.g. <h1>
, <div>
).
Where you place the HTML <noscript>
element depends on how an HTML document behaves without JavaScript. If an HTML document is heavily dependent on JavaScript that it's unreadable without it, it's best to place the <noscript>
element in the <head>
section of document and perform a redirect to another HTML document that doesn't rely on JavaScript with instructions on enabling JavaScript, as illustrated in listing 3-2. If an HTML document is not 100% functional due to a lack of JavaScript but still readable, it's best to place the <noscript>
element at the top of the <body>
section of a document with a banner telling a user to enable JavaScript to get full functionality, as illustrated in listing 3-3.
Listing 3-2. HTML <noscript>
element in <head>
with redirect
<html> <head> <noscript> <meta http-equiv="refresh" content="3;url=/please-enable-javascript.html"> </noscript> </head> <body> ... </body>
Listing 3-3. HTML <noscript>
element in <body>
with warning banner
<html> <head> <style> .no-script-banner { position: fixed; top: 0; left: 0; width: 100%; text-align: center; background-color:#FF5252; color:#fff; font-size:large; padding:10px; } </style> </head> <body> <noscript> <div class="no-script-banner"> <b>Enabling JavaScript offers a better navigation experience</b> </div> </noscript> ... </body>
Listing 3-2 illustrates how a <noscript>
element inside the <head>
section of a document declares a <meta>
element to redirect users to a different page. The <meta>
element uses the http-equiv
attribute with a refresh
value to indicate the use of the HTTP header Refresh
which performs a redirect operation. The pair of values assigned to the content
attribute specify the redirect details: 3
is the number of seconds to wait before making the redirect -- it can be set to 0 for an immediate redirect -- whereas url=/please-enable-javascript.html
indicates the url
to redirect to, which in this case is an absolute path on the site to the page please-enable-javascript.html
.
Listing 3-3 illustrates a <noscript>
element inside the <body>
section of an HTML document, which creates a <div>
element to show a red banner at the top of the page telling users that enabling JavaScript offers a better navigation experience. The <div>
element uses the CSS class
attribute to give the banner its style through the CSS class <no-script-banner>
declared in the <style>
element.
The var
& function
keywords, scope, hoisting and undefined
JavaScript relies on rather obvious keywords to define variables and functions: var
and function
, respectively. This means JavaScript .js
files tend to lead with statements in the form var language = "JavaScript"
or var numbers = [1,2], language = "JavaScript", letters = {"vowels":["a","e","i","o","u"]}
-- the last of which is used to declare multiple variables in one line, as a CSV(Comma Separate Value) list of variables -- as well as function name(args) { }
statements, where name
is the name to reference a function, args
is the function input (if any) and curly brackets {}
are used to wrap the logic of the function.
The var
keyword plays an important role because it influences the scope/visibility of a variable, to be either global or local, as illustrated in listing 3-4.
Listing 3-4. var
scope behavior
var letter = "a"; var number = 1; function testing() { var number = 2; // Check variable values in function scope console.log("testing() number: %d", number); console.log("testing() letter: %s", letter); letter = "e"; } // Run testing function testing(); // Check variable values in global scope console.log("global number: %d", number); console.log("global letter: %s", letter);
Listing 3-4 begins by declaring two global variables, letter
with a value "a"
and number
with a value of 1
. Next, you can see the testing()
function has its own local var number = 2
and it also re-assigns the global letter
variable to a value of "e"
.
The testing()
function is run first, so the initial output is testing() number: 2
and testing() letter: a
. Since the testing()
function has its own var number
statement, this local scope takes precedence and therefore the output for number
is 2
. The output for the letter
variable inside testing()
is taken from the global scope so the result is "a"
. Finally, notice the last statement in the testing()
function updates the global letter
variable to "e"
.
Once the testing()
function runs, the two final console.log
statements output: global number: 1
and global letter: e
. The global number
remains unchanged -- since the testing()
number
variable has its own local definition with var
-- however, the global letter
variable is now "e"
on account of it being updated inside the testing()
function.
Now that you have an understanding of the var
keyword and its influence on the scope of JavaScript variables, it's also important you understand another JavaScript behavior associated with variables called hoisting.
Hoisting is the act of raising or lifting something and in JavaScript all variables are hoisted to the top of their scope. As a consequence, this means variables are processed before any JavaScript logic is executed and in turn variables declared throughout a script or function are equivalent to declaring them at the top of their scope. Listing 3-5 illustrates the process of JavaScript hoisting.
Listing 3-5. var
hoisting behavior
// IF YOU TYPE... // JAVASCRIPT DOES THE FOLLOWING (DUE TO HOISTING) var letter; var letter; var vowels; console.log(letter); console.log(letter); function testing() { function testing() { console.log(number); var number; var number = 2; console.log(number); console.log(number); number = 2; console.log(number); } } testing(); testing(); console.log(vowels); console.log(vowels); var vowels = ["a","e","i","o","u"]; vowels = ["a","e","i","o","u"] console.log(vowels); console.log(vowels); random = 8; random = 8; console.log(random); console.log(random);
As you can see, although the vowels
variable is declared near the bottom, it's hoisted by JavaScript to the top since it's part of the global scope. In addition, the number
variable declared in the middle of the testing
function is also hoisted to the top of the function which is its local scope. Under most circumstances JavaScript hoisting is an afterthought, mainly because variables are generally declared at the top of both the global and local scopes anyways to maintain logical ordering.
However, hoisting in can have unintended behaviors if you're not careful. Hoisting moves variable definitions toward the top of their scope before any logic is executed, this means you can potentially access or use variables before they're declared. But in addition, hoisting only raises a variable's definition and not its actual value, as you can see in the example.
For example, although the vowels
variable in listing 3-5 is declared near the bottom, it's valid -- albeit sloppy -- to have statements like console.log(vowels)
or variable re-assignments (e.g. vowels = []
) before the actual var vowels = ["a","e","i","o","u"];
statement, something that's possible because the var vowels
definition is hoisted toward the top of the scope allowing vowels
operations to run as if the var vowels
definition were actually declared at the top.
This means the first console.log(vowels);
statement in the example -- before the actual var vowels = ["a","e","i","o","u"];
statement -- outputs undefined
, since variable values in an of themselves aren't hoisted. The second console.log(vowels);
statement in the example though does output ["a","e","i","o","u"]
, since it exists after the variable value assignment. This same hoisting behavior occurs on the number
variable inside the testing()
function (i.e. the first console.log(number);
statement outputs undefined
, where as the second console.log(number);
statement outputs a value of 2
.)
As you can see, hoisting can leave you wide open to potential errors. In this case, you can see that even if you attempt to perform an operation on a variable that hasn't been declared, JavaScript doesn't throw an error, instead it happily gives a variable a value of undefined
because hoisting lifts variable definitions toward the top of their scope.
All of which takes us to the subtle difference between undeclared and undefined. In JavaScript, the term undeclared is distinctively different than the term undefined, due to how JavaScript assigns variables their values. For example, in listing 3-5 -- once hoisting is applied -- the statement var vowels;
declares the vowels
variable, but its value is left undefined. Similarly, the var letter;
statement has no explicit assignment and thus the letter
variable is given the value of undefined.
Once a variable is declared in this manner -- due to hoisting or because it isn't assigned an explicit value -- it becomes available for any operation and JavaScript automatically assigns the undefined
primitive data type value to all undefined variables. Because JavaScript data types is a deep topic and I don't want to get sidetracked at this point, the next chapter on JavaScript data types contains more details about the undefined
primitive data type and JavaScript data types in general.
Now look at the random = 8;
statement in the second to last line in listing 3-5. This last statement also represents a variable, but notice it's missing the var
keyword, which makes it an undeclared variable. The issue as you can see in the last line of listing 3-5 console.log(random)
, JavaScript also happily lets you access an undeclared variable, in this case console.log(random)
outputs 8
.
So similar to the unintended consequences of accessing hoisted variables, operations on undeclared variables in JavaScript can also lead to unintended consequences. To restrict operations on undeclared variables (i.e. without the var
keyword) you can rely on JavaScript strict mode. However, undefined variables are still perfectly valid in JavaScript -- since they're an actual JavaScript data type -- which are the result of knowingly not assigning a value to a variable (e.g.var letter
) or a side-effect of a hoisted variable (e.g.var vowels
).
First class functions: Function declarations, function expressions, hoisting & undefined
Functions are a pretty basic concept in all programming languages because they encapsulate logic you can invoke in a single call, but in JavaScript functions play a key role because they're used extensively to support first-class functions, which means functions are values that can be passed around to other functions.
A function declaration is what was shown in the previous example in listing 3-5 and what you're most likely accustomed to see as functions in other languages (e.g.function testing() { }
). Function expressions on the other hand, while similar to function declarations, are declared as var
statements just as if they were variable values. For example, the function declaration function testing() { }
is equivalent to the function expression var testing = function() { }
. With an understanding that function declarations can have equivalent function expressions, let's take a closer look at the ability to use JavaScript functions as values.
Listing 3-6. Functions as values
function plainEchoer(message) { console.log(message); return message; } plainEchoer("Hello there from plainEchoer()!"); console.log("Now passing a function as value from plainEchoer()..."); plainEchoer(plainEchoer(plainEchoer(plainEchoer("Hello there from plainEchoer()!")))); var echoer = function(message) { console.log(message); return message; } echoer("Hello there from echoer()!"); console.log("Now passing a function as value from echoer()...") echoer(echoer(echoer(echoer("Hello there from echoer()!"))));
As you can see, plainEchoer()
is a function declaration (i.e. it begins with the function
keyword) that outputs and returns its input value. The interesting aspect of this function occurs when it's called in the form plainEchoer(plainEchoer(plainEchoer(plainEchoer("Hello there from plainEchoer()!"))));
, illustrating JavaScript's first-class function status. Notice it isn't necessary to use a placeholder variable to pass the result of a function to another function, in JavaScript you can simply chain function calls and the functions themselves are treated as values.
The second part of the example illustrates the echoer
function expression (i.e. it begins with the var
keyword and its value is a function()
statement). The echoer
function expression also outputs and returns its input value, just like the plainEchoer()
function expression. Similarly, you can see it's possible to treat function expressions as first-class functions, by chaining multiple calls in the form echoer(echoer(echoer(echoer("Hello there from echoer()!"))));
.
Now that you have an understanding of JavaScript's ability to treat functions as values (a.k.a. first-class functions), you're likely to ask yourself, when should you use a function declaration and when should you use a function expression ? It turns out you can achieve the same results with both function declarations and function expressions, as you've just seen in the previous example in listing 3-6. However, function expressions are often the preferred choice over function declarations, since the former provide added functionality -- via Immediately-invoked function expressions (IIFE), which I'll describe shortly -- in addition to being less error prone in terms of scope/visibility issues.
Now let's take a look at these scope/visibility issues when it comes to using function declarations and function expressions.
Listing 3-7. Hoisting behavior with function declarations and expressions
// IF YOU TYPE... // JAVASCRIPT DOES THE FOLLOWING (DUE TO HOISTING) console.log(helloDeclaration()); function helloDeclaration() { function helloDeclaration() { return "Hello from a function declaration"; return "Hello from a function declaration"; } } console.log(helloDeclaration()); var helloExpression; console.log(helloDeclaration()); //helloDeclaration function is hoisted console.log(helloDeclaration()); // Can't call function expression before its defined // Raises TypeError: helloExpression is not a function console.log(helloExpression()); console.log(helloExpression()); var helloExpression = function() { helloExpression = function() { return "Hello from a function expression"; return "Hello from a function expression"; } } console.log(helloExpression()); console.log(helloExpression());
The first thing to note about function declarations is they're hoisted to the top of their scope just like variables. If you look closely at the example in listing 3-7, you can see the helloDeclaration()
function is called both before and after the actual function declaration definition. Although in some programming languages calling a function before it's defined generates an error, in JavaScript you can call a function declaration at any point -- so long as it's within scope -- and it's perfectly valid due to the effect of hoisting.
Because function expressions are declared as var
statements, they're also subject to variable hoisting as described in the previous section. But similarly, like all other var
statements, hoisting only raises a function expression's definition and not its actual value, as you can see in the example. This means the helloExpression()
function expression variable is set to undefined
at the outset and the function expression only becomes available at the point of the actual function expression definition.
As you can see in the example in listing 3-7, because the first attempt to call helloExpression()
is done before the function expression definition, JavaScript generates the error TypeError: helloExpression is not a function
because helloExpression()
at this point it's undefined. The reason undefined function expressions generate an error is because you can't call/invoke an undefined function, unlike an undefined variable which you can easily use and access -- since it's assigned an undefined
primitive data type -- as you learned in the previous section.
As you can see from this example listing 3-7, calling function expressions before they're defined causes an error, something which is a good safety net vs. function declarations which are entirely hoisted (i.e. definition and value) to the top of their scope. This is why I mentioned function expressions are less error prone in terms of scope/visibility issues than function declarations.
Working with text: '
and "
; plus template literals with backticks `
and ${}
JavaScript variables can declare text in various ways. It's equally valid to use either single quotes '
or double quotes "
to enclose text values. One thing to consider when selecting one option over the other, is if you plan to literally use either symbol in a text value. For example, if a text value will contain an apostrophe as a single quote, it's easier to use double quotes to enclose the text value. Similarly, if a text value will contain a sentence in double quotes, it's easier to use single quotes to enclose the text value. Both of these cases are illustrated in listing 3-8.
Although it reduces readability, it's also possible to use a literal single quote in a value delimited by single quotes or a literal double quote in a value delimited by double quotes. In order for this to work, a literal single quote or literal double quote must be escaped with a backslash \
. Notice in listing 3-8, the plain text variables and their equivalent escaped quote ones produce identical values.
Another scenario to consider for JavaScript text variables is values that can span multiple lines. The simplest option is to merge all lines into a single line, however, this can reduce readability. There are two other alternatives to declare text in multiple lines, while keeping individual lines visually separate. One option is to use a backslash \
as the line separator in a single quoted or double quoted value. While a second alternative is to use the +
symbol, to concatenate standalone lines with their own single quoted or double quoted delimiters. Listing 3-8 illustrates all three variations for multi-line text values, which is worth pointing out produce identical values.
Listing 3-8. Text with '
, "
, backslashes and multiple lines
var russel = "Science is what you know. Philosophy is what you don't know"; var descartes = 'I think therefore I am ("Cogito, ergo sum")'; console.log(russel); console.log(descartes); var russelBackslash = 'Science is what you know. Philosophy is what you don\'t know'; var descartesBackslash = "I think therefore I am (\"Cogito, ergo sum\")"; console.log(russelBackslash); console.log(descartesBackslash); if (russel == russelBackslash) { console.log("russel == russelBackslah"); } if (descartes == descartesBackslash) { console.log("descartes == descartesBackslah"); } var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; var loremBackslash = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\ "; var loremConcatenation = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + " Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + " Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + " Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; console.log(lorem); console.log(loremBackslash); console.log(loremConcatenation); if (lorem == loremBackslash) { console.log("lorem == loremBackslah"); } if (loremBackslash == loremConcatenation) { console.log("loremBackslash == loremConcatenation"); }
The +
symbol that concatenates multiple lines in listing 3-8, can also be used to dynamically create text values based on variables. For example, a variable statement like var name;
can be concatenated with another variable like var greeting = "Hello " + name + ", how are you today?"
, in this manner the greeting
variable is dynamically updated with the name
value.
As useful as the +
symbol is to concatenate fixed text with variables, these type of statements can be difficult to read. In addition, the options to join text spanning multiple lines -- shown in listing 3-8 -- using either +
, backslashes \
or merging all lines into a single line, can also be difficult to construct and read. In order to solve these issues, JavaScript supports a third delimiter for text values in the form of a backtick `
.
Variables that use the backtick `
symbol to delimit text, receive the special name of template literals or tagged templates, depending on their use. Unlike the '
and "
symbols, text delimited by backticks (a.k.a. backquotes) can span multiple lines without any special syntax and can also contain inline JavaScript (e.g. variables references or plain logic) using the ${}
syntax making it easier to incorporate dynamic values. Listing 3-8 illustrates the use of backticks as template literals and tagged template literals (or simply 'tagged templates' for short).
Listing 3-9. Template literals and tagged templates with backticks
var vowels = ["a","e","i","o","u"]; // Template literal var message = `Vowel letters are ${vowels}`; console.log(message); var cost = 5; var tax = 7.25; // Template literal console.log(`Total cost with tax: ${cost * (1 + tax/100)}`); function worldMessage(message) { // Template literal console.log(`${message} World!`) } worldMessage("Hello"); worldMessage("Brave new"); // Function for tagged template literal function thankYouEmail(parts,recipient="Customer",sender="Support") { return `${parts[0]}${recipient}${parts[1]}${sender}`; } // Define parameters to use in template literal var recipient, sender; // Call tagged template literal var boilerplateEmail = thankYouEmail`Dear ${recipient}, Thank you for contacting us... Best, ${sender}`; console.log(boilerplateEmail); // Redefine parameters to use in template literal recipient = "Jen"; sender = "Sally"; // Call tagged template literal var personalEmail = thankYouEmail`Hi ${recipient}, I read... Thanks, ${sender}`; console.log(personalEmail);
The first example in listing 3-9 uses a backtick on the message
reference with the ${vowels}
syntax to insert the value of the vowels
variable into the text. The second backtick example in listing 3-9 also uses the ${}
syntax, but notice how the statement contains multiple references and also uses a mathematical operation (e.g. ${cost * (1 + tax/100)}
) to become part of the text. The third example in listing 3-9, demonstrates how it's also possible to use template literals inside functions and use function parameters as part of the substitution process.
The last two examples in listing 3-9 illustrate the use of a tagged template named thankYouEmail
. In this case, notice the last two backticked examples use the same template literal syntax (i.e. ${}
), but are prefixed with thankYouEmail
, that is, the name of the tagged template. Tagged templates work like functions, so the first thing a JavaScript engine does when it encounters the thankYouEmail` `
statement is to look for a function named thankYouEmail
.
Functions used for tagged templates need to follow two conventions: Use an input signature in the form ([literal_text_parts], reference_1, reference_2, reference_etc)
, where literal_text_parts
represents an array containing the text of the tagged template separated by references and reference_1
, reference_2
& reference_etc
are the references (i.e. ${}
) declared in the text of the tagged template; in addition, the function must also return a result for the tagged template, using any of the input variables or some other construct.
In this case, you can see the thankYouEmail
function declares three arguments: parts
, recipient="Customer"
, sender="Support"
. In addition, the function also opts to return a result with all the input values with another backticked statement, however, it's perfectly valid for a function backing a tagged template to return anything (e.g. a boolean value based on the input values, a trimmed down version of the input values or some other variation).
In the first tagged template call (i.e. boilerplateEmail
), the parts
argument is assigned the ["Dear ", "Thank you for contacting us..."]
value, since "Dear "
is the first part of text in the template, followed by the ${recipient}
reference which counts as a separator, followed by "Thank you for contacting us..."
as the second part of the text in the template. Since both the recipient
and sender
references are undefined
at the time of invoking the tagged template, the recipient
and sender
parameters are assigned the default function values "Customer"
and "Support"
, respectively. In the second tagged template call (i.e. personalEmail
), the message
argument is assigned the ["Hi ", "I read..."]
value, while the recipient
argument is assigned "Jen"
-- since this value is assigned prior to invoking the tagged template and overrides the default function value -- and the sender
argument is assigned "Sally"
since this value is also assigned prior to invoking the tagged template and also overrides the default function value. As mentioned in the last paragraph, the output for both tagged template calls is a concatenation of all input values, replicating the original template literal.
Text and escape sequences: Backslash \
, \x
and \u
; plus \u{}
You can sometimes find JavaScript text interspersed with escape sequences. An escape sequence is a combination of characters intended to represent something other than their literal value. Escape sequences in JavaScript are prefixed with a backslash \
(a.k.a. escape character) -- to identify them as such -- followed by a predetermined character or set of characters.
In JavaScript you can find several escape sequences: those prefixed with a backslash \
and one character that can be either literal character escapes or control character escapes; those prefixed with \x
followed by more characters known as hexadecimal escapes; those prefixed with \u
followed by more characters known as unicode escapes; and those prefixed with \u{}
with characters inside {}
known as unicode code point escapes.
Literal character escape sequences are used for cases when a character holds special meaning in the context of a text definition and needs to be used literally as part of the text definition. In listing 3-8 you already explored a couple of literal character escape sequences -- \'
and \"
-- needed to incorporate a single quote '
and double quote "
, since such characters hold special meaning to delimit text definitions. Other literal character escape sequences can include \\
to literally represent a backslash \
and \`
to literally represent a backtick `
. Although you can find literal character escape sequences in plain text variables, they're particularly heavily used to define regular expressions -- which define text patterns to match values inside text variables -- in which case you'll find an even larger variety of literal character escape sequences, since regular expressions rely on more special meaning characters (e.g. \[
, \]
or \?
are used to literally match [
, ]
and ?
, respectively, since such characters in regular expressions hold special meaning for regular expression definitions).
Control character escape sequences are used for cases when you want to represent a non-printable character in the context of a text definition. Non-printable characters include such things as: a backspace represented with the escape sequence \b
; a new line represented with the escape sequence \n
; or a tab represented with the escape sequence \t
. Table 3-2 illustrates the full set of JavaScript control character escape sequences, including JavaScript's most common literal character escape sequences.
Table 3-2. JavaScript literal & control character escape sequences
Escape sequence type | Escape sequence | Represents |
---|---|---|
Literal character escape sequence | \' | Literal single quote ' , used when ' has special meaning (e.g. text or regular expression that uses single quotes) |
\" | Literal double quote" , used when " has special meaning (e.g. text or regular expression that uses double quotes) | |
\` | Literal backtick ` , used when ` has special meaning (e.g. text or regular expression that uses backticks) | |
\\ | Literal backslash \ , used when \ has special meaning (e.g. text or regular expression that uses a backslash) | |
\[ | Literal left bracket [ , used when [ has special meaning (e.g. regular expression to match a left bracket) | |
\] | Literal right bracket ] , used when ] has special meaning (e.g. regular expression to match a right bracket) | |
\? | Literal question mark ? , used when ? has special meaning (e.g. regular expression to match a question mark) | |
Control character escape sequence | \b | Backspace |
\f | Form feed (page break) | |
\n | Line feed (new line) | |
\r | Carriage return | |
\t | Horizontal tab | |
\v | Vertical tab | |
\0 | The null character* | |
*The \0 escape sequence or null character is a control character used by character encodings across many programming languages, which is unrelated to JavaScript's null primitive |
Understanding hexadecimal escape sequences which start with \x
, unicode escape sequences which start with \u
and unicode code point escape sequences which start with \u{}
require some lower level background into how JavaScript handles text.
JavaScript engines internally use a character encoding format called UTF-16. In very simle terms, UTF-16 uses a concept called code point to represent characters, where each code point is defined by a number. With UTF-16 being a base 16 system, this means code point numbers use a hexedecimal notation with a 0
to 9
or A
to F
sequence to represent code points in multiples of 16.
This essentially means all characters can also be represented as code point numbers. Listing 3-10 shows how it's possible to define text using literal characters -- like humans do -- or use either a hexadecimal escape sequence, a unicode escape sequence or a unicode code point escape sequence, more importantly, listing 3-10 also illustrates how all approaches produce the same results.
Listing 3-10. Text escape sequences
// single character with escape sequences let vowel = "a"; let vowelHex = "\x61"; let vowelUnicode = "\u0061"; let vowelUnicodeCP = "\u{0061}"; console.log("vowel value is: %s", vowel); console.log("vowelHex value is: %s", vowelHex); console.log("vowelUnicode value is: %s", vowelUnicode); console.log("vowelUnicodeCP value is: %s", vowelUnicodeCP); if ((vowel == vowelHex) && (vowelHex == vowelUnicode) && (vowelUnicode == vowelUnicodeCP)) { console.log("((vowel == vowelHex) && (vowelHex == vowelUnicode) && (vowelUnicode == vowelUnicodeCP))"); } // multiple characters with escape sequences let x = "JavaScript"; let xHex = "\x4A\x61\x76\x61\x53\x63\x72\x69\x70\x74"; let xUnicode = "\u004A\u0061\u0076\u0061\u0053\u0063\u0072\u0069\u0070\u0074"; let xUnicodeCP = "\u{004A}\u{0061}\u{0076}\u{0061}\u{0053}\u{0063}\u{0072}\u{0069}\u{0070}\u{0074}"; console.log("x value is: %s", x); console.log("xHex value is: %s", xHex); console.log("xUnicode value is: %s", xUnicode); console.log("xUnicodeCP value is: %s", xUnicodeCP); if ((x == xHex) && (xHex == xUnicode) && (xUnicode == xUnicodeCP)) { console.log("((x == xHex) && (xHex == xUnicode) && (xUnicode == xUnicodeCP))"); }
Listing 3-10 first illustrates how the "a"
character is equivalent to the hexadecimal escape sequence "\x61"
, the unicode escape sequence \u0061
and the unicode code point escape sequence \u{0061}
. The second set of definitions in listing 3-10 also shows how it's possible to represent the longer "JavaScript"
text using the three different escape sequence syntax types and obtain the same outcome.
Although you're unlikely to see or use JavaScript escape sequences/code points to encode letters available on keyboards -- like those shown in listing 3-10 -- what you're more likely to see or use is escape sequences for more specialized characters you want to incorporate into JavaScript text. This of course leads us to explore why there are three different escape sequences to achieve the same results, an answer that's rooted in the limitations and base 16 nature of each escape sequence:
- Hexadecimal escape sequences that start with
\x
are limited to representing 256 code points. - Unicode escape sequences that start with
are initially limited to representing 65,536 code points, but can support up to 1,112,064 code points through special surrogate pair syntax.\u
- Unicode code point escape sequences that start with
\u{}
can support up to 1,112,064 code points with simple syntax.
Hexadecimal escape sequences always consist of two elements added to its \x
prefix. This means the lowest hexadecimal escape sequence is \x00
, followed by \x01
, moving on to \x0F
(16th value), continuing with \x10
, \x11
and \x1F
(32th value), finishing with \xFD
, \xFE
and \xFF
(256th value). If you add something outside these boundaries you'll either get an error or the interpretation of only the two elements that follow the \x
prefix . For example, if you add elements that are not between 0
and 9
or A
and F
, you'll get the JavaScript error Invalid hexadecimal escape sequence. Similarly, if you add a third element like \xFF1
, then the \xFF
escape sequence is interpreted followed by the trailing element, so \xFF1
= ÿ1
since the \xFF
escape sequence represents the ÿ
character.
This two element cap on hexadecimal escape sequences means they're limited to representing 256 characters or code points. Where the first 32 code points (0-31) represent ASCII control characters, like those presented in the second half of table 3-2 that can also be represented with JavaScript control character escape sequences. The following 96 code points (32-127) are ASCII printable characters -- like the code points used in listing 3-10 -- and the remaining 128 code points (128-255) are extended ASCII characters. However, as you saw in listing 3-10, the same code points used by hexadecimal escape sequences are equivalent to those used by unicode escape sequences, with the advantage of unicode escape sequences being capable of supporting more than 256 code points.
Unicode escape sequences always consist of four elements added to its \u
prefix. This means the lowest unicode escape sequence is \u0000
, followed by \x0001
, moving on to \u000F
(16th value), continuing with \u0010
, \u0011
and \u001F
(32th value), finishing with \uFFFD
, \uFFFE
and \uFFFF
(65,536th value). If you add something outside these boundaries you'll either get an error or the interpretation of only the four elements that follow the \u
prefix . For example, if you add elements that are not between 0
and 9
or A
and F
, you'll get the JavaScript error Invalid Unicode escape sequence. Similarly, if you add a fifth element like \uFFFC1
, then the \uFFFC
escape sequence is interpreted followed by the trailing element, so \uFFFC1
= 1
since the \uFFFF
escape sequence represents the 
character.
This four element cap on unicode escape sequences means they're initially limited to representing 65,536 code points. This 65,536 limit comes from UTF-16 using a single 16-bit element or code unit to store a code point that represents a character. However, it's entirely possible for UTF-16 to use a second 16-bit element or code unit to expand the amount of code points it can support from 65,536 to 1,112,064.
The key to unlocking support from 65,536 to 1,112,064 code points in JavaScript unicode escape sequences prefixed with \u
, as well as understanding what brought about the need for JavaScript unicode code point escape sequences prefixed with \u{}
, is related to how UTF-16 uses planes through a second 16-bit element or code unit.
In UTF-16, relying on a single 16-bit element or code unit means code points can go from a low of U+0000
to a high of U+FFFF
(65,536th value). This set of code points in UTF-16 that use a single code unit are known as the "Basic multilingual plane" (BMP) or plane 0, which are code points representing characters and symbols used in most modern languages. In order to support more code points, UTF-16 makes use of its other 16-bit element or code unit to support sixteen planes (a.k.a. supplementary planes) with each plane capable of supporting a maximum 65,536 code points. In this manner, code points going from a low of U+10000
to a high of U+1FFFF
belong to plane 1 or "Supplementary multilingual plane", code points going from a low of U+20000
to a high of U+2FFFF
belong to plane 2 or "Supplementary ideographic plane", moving on to code points going from a low of U+F0000
to a high of U+FFFFF
that belong to plane 15 and finishing with code points going from a low of U+100000
to a high of U+10FFFF
that belong to plane 16.
Although the concept of UTF-16 planes unlocks the ability to move beyond 65,536 code points, this requires support for code points with a fifth or sixth element with ranges that can span from U+10000
to U+10FFFF
. This of course creates a problem for JavaScript unicode escape sequences that are capped to representing four element code points. Because JavaScript isn't the only language to potentially be limited from using five or six elements to represent code points, UTF-16 planes are also supported through what are known as surrogate pairs.
Instead of using a single five or six element escape sequence to represent a code point made up of two code units, it's possible to use a pair of four element escape sequences each one representing a code unit to also represent a code point. This pair of four element escape sequences, known as a surrogate pair, is composed of a high-surrogate code unit and a low-surrogate code unit, in this manner, if two four element escape sequences are found back to back and they match an expected high-surrogate/low-surrogate pattern, both escape sequences are interpreted as a single code point. High-surrogate code units are those composed in the escape sequence range U+D800
0+DBFF, whereas low-surrogate code unit values are those composed in the escape sequence range U+DC00
to U+DFFF
. Therefore, if you see a pair of JavaScript unicode code point escape sequences in the form \uD***\uD***
it's likely you're seeing a surrogate pair representing a single code point.
Due to the reduced readability and complexity that can arise in structuring UTF-16 surrogate pairs, JavaScript ES6 (ES2015) incorporated the unicode code point escape sequence with the \u{}
syntax, where the input to {}
is capable of directly accepting a five or six element escape sequence with plane information to represent a code point. This makes unicode code point escape sequences the preferred choice for many situations vs. dealing with surrogate pairs in plain unicode escape sequences or dealing with the limitations of hexadecimal escape sequences.
Listing 3-11 illustrates how a unicode code point escape sequence simplifies representing a character vs. using a unicode surrogate pair.
Listing 3-11. Text escape sequences with unicode surrogate pairs and equivalent unicode code point escape sequences
// literal, unicode surrogate pair and unicode code point let clef = "𝄞"; let clefUnicode = "\uD834\uDD1E"; let clefUnicodeCP = "\u{1D11E}"; console.log("clef value is: %s", clef); console.log("clefUnicode value is: %s", clefUnicode); console.log("clefUnicodeCP value is: %s", clefUnicodeCP); if ((clef == clefUnicode) && (clefUnicode == clefUnicodeCP)) { console.log("((clef == clefUnicode) && (clefUnicode == clefUnicodeCP))"); } // literal, unicode surrogate pair and unicode code point let emoji = "😅"; let emojiUnicode = "\uD83D\uDE05"; let emojiUnicodeCP = "\u{1F605}"; console.log("emoji value is: %s", emoji); console.log("emojiUnicode value is: %s", emojiUnicode); console.log("emojiUnicodeCP value is: %s", emojiUnicodeCP); if ((emoji == emojiUnicode) && (emojiUnicode == emojiUnicodeCP)) { console.log("((emoji == emojiUnicode) && (emojiUnicode == emojiUnicodeCP))"); }
Listing 3-11 illustrates a pair of characters that require two code units for their code point representation. The musical clef symbol "𝄞"
is first declared literally, followed by its unicode escape sequence with the surrogate pair "\uD834\uDD1E"
and the unicode code point escape sequence "\u{1D11E}"
. Next, the "😅"
emoji is declared literally, followed by its unicode escape sequence with the surroage pair "\uD83D\uDE05"
and the unicode point sequence "\u{1F605}"
. Notice the conditionals confirm the three character representations for literal, unicode escape sequence and unicode code point sequence are identical.
Equality symbols ==
, !=
, ===
and !==
By most accounts, JavaScript follows the same equality syntax conventions as many other programming languages. For example, the =
symbol is used to assign values, such as var number = 1;
which gives the number
variable a value of 1
. In addition, JavaScript also uses the computer science symbol convention of ==
and !=
to perform equality comparisons. For example, if (number == 1)
tests if the number
variable has a value of 1
and if (number != 1)
tests if the number
variable does not have a value of 1
.
Where JavaScript strays from the conventional path is with its use of the ===
and !==
symbols, also known as strict equality operators. In JavaScript, the standard computer science equality symbols ==
and !=
symbols, are known as loose equality operators.
The reason JavaScript supports both double equal and triple equal symbols for comparison operations, is rooted in JavaScript data types. As it turns out, JavaScript has two major groups of data types: primitive and object types. The standard computer science equality symbols ==
and !=
symbols are used to perform comparisons by value, irrespective of the underlying data type, in other words, the ==
and !=
symbols implicitly perform data type conversions to compare values. Where as the triple equal ===
and !==
symbols are used to perform comparisons without any type conversion, in other words, comparisons made with the ===
and !==
symbols contemplate the underlying JavaScript data type. Listing 3-12 illustrates this behavior.
Listing 3-12. Equality and inequality symbols
if (6 == "6") { // True console.log('6 == "6"'); } if (["a","e"] == "a,e") { // True console.log('["a","e"] == "a,e"'); } if (6 === "6") { // False, won't enter condition console.log('6 === "6"'); } if (6 === 6) { // True console.log('6 === 6'); }
Notice how the first JavaScript comparison in listing 3-12 statement 6 == "6"
is true, even though the comparison is being made between a 6
integer value and a quoted "6"
that represents a string value. The second JavaScript comparison statement ["a","e"] == "a,e"
is also true, even though the first value is an array with two letters and the second value is a string with the same two letters. In both cases, because the statements use a double equal symbol (i.e. ==
or !=
) they ignore the data types for the variables in question and a comparison is made based on value, therefore both 6 == "6"
and ["a","e"] == "a,e"
evaluate to true.
The third JavaScript statement 6 === "6"
uses a strict equality operator and therefore when a comparison is made between a 6
integer value and a quoted "6"
the condition evaluates to false. The fourth JavaScript statement 6 === 6
evaluates to true, since both variables represent the same value and data type (integer). This behavior associated with a triple equal symbol (i.e. ===
or !==
) is based on JavaScript performing a comparison based on not just variable values but also the underlying data types.
For the moment, this level of detail on JavaScript strict equality and loose equality operators should be sufficient. The upcoming topic dedicated to JavaScript data types re-addresses the use of JavaScript strict equality and loose equality operators in greater detail, concepts that should be much easier to grasp at that point, once you know about JavaScript's various data types.
Debugging, documenting & logging JavaScript
While the intent of writing any programming language is to solve problems, it's generally inevitable to end up doing some ancillary tasks to support this problem solving process. These tasks can take the form of debugging complex workflows, which in itself can require extensive use of logging statements, as well as the need to document JavaScript logic for either oneself or future maintainers.
The console
object
JavaScript or more specifically ECMAScript doesn't define a standard logging syntax, therefore JavaScript uses a logging technique that's historically rooted in browsers. If you look back at figure 3-1 and figure 3-2 you'll see the console facilities included in browsers that work as REPLs. These same console facilities are also intended to view log messages from JavaScript running on browsers.
In order to interact with a browser's console you can use the console
object. The console
object is globally accessible as a convenience in JavaScript, therefore, it's as simple as typing console
in a REPL or introducing a console
statement in a .js
file to get a list of methods offered by the console
object.
Given the prevalent use of the console
object in JavaScript -- similar to the HTML <script>
element -- there's sufficient agreement between vendors for a console
specification[7] to ensure its functionalities work equally across environments. Inclusively, even non-browser JavaScript environments such as Node JS and Deno, which lack a console facility per se like browsers, have gone to the extent of supporting the console
object in a similar fashion, albeit such environments log console
statements to standard output (e.g. the command line where the environment is run).
In previous examples, you can see the console.log()
method is widely used to output generic log messages. But in addition, the console
object also supports other logging methods that map closely to the standard logging practice in other programming languages, where different levels are used to output deeply detailed log messages (e.g. debug) or to limit output to severe log messages (e.g. error).
conosle.debug()
.- Generates a debug log message.console.info()
.- Generates an informative log message.console.log()
.- Generates a generic log message.console.warn()
.- Generates a warning log message.console.error()
.- Generates an error log message.
There are two benefits to using these other console
logging methods. First, it's possible to configure a browser console to output certain logging levels, this way the logging output can be controlled to only show certain log messages. The second benefit of mixing up the use of console
logging methods is the log messages are color formatted depending on their severity (e.g. console.error()
in red; console.warn()
in yellow), adding a nice touch to visually identify the importance of log messages.
All the console
logging methods from the previous list also support a basic C-like sprintf formatting technique -- initially shown in listing 3-4 -- whereby a log message can use format specifiers, which are substituted with arguments passed to a logging method. The following list shows the supported format specifiers with examples:
%s
.- Substitutes a string value (e.g.console.log("Processing %s by %s", task, name)
, thetask
value gets inserted in the first%s
and thename
value into the second%s
).%d
or%i
.- Substitutes an integer value (e.g.console.log("%d items", count)
, thecount
value gets inserted in%d
).%f
.- Substitutes a float value (e.g.console.log("Total: %f", sale)
,sale
gets inserted in%f
).%o
or%O
.- Substitutes an object value (e.g.console.log("Cart contents %O", cart)
, thecart
value gets inserted in%O
).%c
.- Substitutes a CSS value (e.g.console.log("%cStatement in fuchsia: %s", "background-color:#FF00FF;font-size:large;font-weight:bold", message)
, the CSS properties in the fixed string get inserted in%c
and themessage
value gets inserted in%s
; in this case the console message is output with a fuchsia background in large and bold font).
Tip An alternative to outputting the contents of an object with the%o
or%O
specifiers, is to wrap a reference with theJSON.stringify()
method and use the%s
specifier. For example, the output for the statementconsole.log("Object is %s", JSON.stringify(myobject)
) is a string version of the statementconsole.log("Object is %o", myobject)
.
In addition to supporting the logging methods with formatting specifiers presented in the previous two lists, the console
object also supports another series of methods. Unlike the previous console methods intended to output custom log messages, these other methods are designed for a variety of purposes including: formatting the console itself, shortcuts for logging-type information and methods to measure time.
console
formatting methods for the console itself:console.clear()
.- Clears all the data in the console.console.group()
.- Indents all console statements after its presence. Works similarly to declaring an HTML<ul>
element.console.groupCollapsed()
.- Indents all console statements just likeconsole.group()
, but it collapses the content requiring users to click to explore the indented content.console.groupEnd()
.- Outdents all console statements after its presence, used to return output as it was before aconsole.group()
orconsole.groupCollapsed()
statement. Works similarly to declaring an HTML</ul>
element.
console
shortcut methods to output logging information:console.assert(clause)
.- Outputs an error message if the providedclause
(e.g.x > 3
,status == "passed"
) is false. By default, the error message is Assertion failed: console.assert, however, an optional second argument can be used to specify a custom error message (e.g.assert.console(status == "passed", "status is not passed");
outputs Assertion failed: status is not passed if thestatus
value is notpassed
).console.count(label)
.- Provides an incremental counter for a givenlabel
name each time the statement is run. If nolabel
name is provided the counter is nameddefault
(e.g.console.count()
outputsdefault: 1
, a second timeconsole.count()
outputsdefault: 2
;console.count("loop")
outputsloop: 1
;console.count(letter)
whenletter = "a"
outputsa: 1
).console.countReset(label)
.- Resets the incremental counter for a givenlabel
name set byconsole.count(label)
, if nolabel
name is provided thedefault
counter is reset.console.dir(object)
.- Outputs an object's hierarchy of properties and methods. This method produces the same output as the%o
or%O
format specifiers used in logging message methods.console.dirxml(reference)
.- Outputs the hierarchy of HTML/XML elements from a provided JavaScript reference to an HTML/XML element. If a non-HTML/XML reference is provided, the method attempts to output the object's hierarchy of properties and methods (i.e. it will work likeconsole.dir()
).console.table(array|object)
.- Outputs a table structure from a provided array or object, optionally a second argument as an array can specify which columns to output (e.g.console.table(data)
outputs the contents of thedata
array/object in a table structure;console.table(data, ["name","age"])
outputs the contents of thename
andage
columns in thedata
array/object in a table structure.console.trace(object)
.- Outputs the stack trace for a givenobject
.
console
methods to measure time:console.time(label)
.- Starts a timer under a givenlabel
name. If nolabel
name is provided the timer is nameddefault
. Note that this method just starts a timer and doesn't output anything.console.timeLog(label)
.- Outputs a split time for a given timer namedlabel
or if nolabel
name is provided the split time is for thedefault
timer (e.g.console.timeLog()
outputsdefault: 1.234130859375 ms
).console.timeEnd(label)
.- Stops the timerlabel
or if nolabel
name is provided stops thedefault
timer, in addition this statement also outputs the total time from start to finish for the timer (e.g.console.timeEnd()
outputsdefault: 2.43896484375 ms
).
Tip It's good practice to remove allconsole
statements before JavaScript is released to production. The reason is that even though you can control the console (e.g. logging level) on your own browser, there's no way to control the console on other browsers. So with the exception of severe logging messages -- which can be leveraged between an end user and support team -- there should be a minimum ofconsole
object statements in a production release. This removal task ofconsole
statements is often delegated to a JavaScript bundler, that through a simple configuration parameter strips such statements in preparation for a production release.
Tip Although theconsole
object's functionalities follow a specification[7], there are non-standard features available in certain environments that can be very useful (e.g.console.profile()
&console.profileEnd()
are used to record a performance profile, both methods are available in the Google Chrome browser, but not in Node JS).
The debugger
keyword
While the console
object offers a great deal of functionality to analyze JavaScript workflows, sometimes a deluge of log messages or time metrics can be of little value if you're not able to do inspections for function/variable values & scope visibility or call stack analysis in certain problem sections. JavaScript supports the debugger
keyword that acts as a breakpoint to invoke a debugger, where you're able to move forward or backward on a statement by statement basis to analyze JavaScript statements in greater detail.
It's as simple as adding the standalone debugger;
statement to a JavaScript workflow to invoke a debugger, if one is available, otherwise debugger;
statements are ignored. Most browsers -- like Google Chrome and Firefox -- include a debugger by default, so when a debugger;
statement is reached, the debugger opens in the bottom right where can you control the workflow and get details associated with the breakpoint. If you're using a non-browser JavaScript environment like Node JS, you must explicitly run it in inspect mode for debugger;
statements to be evaluated and funneled to the built-in debugging client, as described in the Node JS node inspect
command.
Comments with //
and /* */
In case you didn't notice from previous examples, JavaScript supports two kinds of comments: //
and /* */
A double slash //
defines an in-line comment, with anything after it ignored by a JavaScript engine. This means you can have full line comments if they start with //
or you can also have comments after a statement, for example: var x = 1; // This is a comment after var
.
Although it's perfectly valid to declare one comment line //
after another //
to obtain multiple comment lines, wrapping comments that span multiple line inside /*
and */ is a best practice. Therefore, anything after the /*
symbols is ignored by a JavaScript engine, until the corresponding closing */
symbols are encountered.
Documenting with JSDoc
Although plain comments are a reasonable approach to documenting JavaScript, using a structured format favors documentation that's more readable, as well as the possibility to auto-generate documentation to read it outside a codebase (e.g. as HTML documents). Although unrelated to ECMAScript standards, documenting JavaScript has evolved around JSDoc[8].
JSDoc relies on markup embedded in JavaScript multiline comments. However, there's a minor change needed for multiline comments to be inspected for JSDoc markup, you must add an extra *
to the beginning of a comment, that is, /** */
. This in turn allows all other multiline comments to be ignored, speeding up the JSDoc generation process by only inspecting blocks that start with /**
.
Any text written inside a /**
block is considered a description of the construct below it (e.g. a /** */
block above a var
statement is considered a variable's description). In addition to the description, a /**
block can also include more than 60+ block tags which are prefixed with an @
and intended to describe a construct. For example, there's a @class
block tag to mark a construct as a JavaScript class, @param
and @returns
block tags to specify a function's parameters and return values, as well as block tags like @author
and @since
to add author and versioning information.
Depending on the block tag, it can be declared in a standalone manner (e.g. @global
to declare a global construct), with accompanying text (e.g. @param vowels - An array of vowels
) or with references and accompanying text (e.g. @param {string[]} vowels - An array of vowels
). Curly brackets {}
in JSDoc are used to link to JavaScript references, (e.g. {string}
to indicate a JavaScript string type or {Vowels}
to indicate a JavaScript class named Vowels
). In addition, certain block tags like @param
can also include brackets []
to define optional parameters with default values (e.g. @param {string} [address] - An optional address
or @param {string} [address="N/A"] - An optional address with default value
).
Once you define JSDoc markup in .js
files you can use a variety of tools to generate documentation from JavaScript sources. JSDoc itself comes equipped with a command line tool to generate HTML documentation from a basic template. And for modern environments like Deno, it even comes equipped with its own built-in document generator (e.g. deno doc
) that processes JSDoc markup.
Validating and error handling in JavaScript
JavaScript can be a pretty unforgiving language syntax wise, where a single misplaced quote or comma can break an entire application with thousands of statements. Therefore, it's essential to know how to preemptively validate and apply error handling techniques to limit the potential blast radius of JavaScript errors.
The "use strict"
statement
The "use strict"
statement is added to the beginning of .js
files or functions to enforce strict JavaScript mode[9]. The need for strict JavaScript mode emerged because of the leeway allowed in ECMAScript 5 vs. the requirements of subsequent ECMAScript versions and what were considered innocuous errors. In other words, soon after ECMAScript 5 was formalized, there was a pressing need to fix issues without waiting for a newer ECMAScript version.
Adding "use strict"
to the top of a .js
file or function, enforces the following:
- Generates explicit JavaScript errors, for errors that were previously silent.
- Ensures JavaScript optimizations can be performed by prohibiting or generating errors for certain constructs.
- Protects against the use of keywords and syntax that would conflict with future ECMAScript versions.
Let's revisit the example in listing 3-5 which shows undeclared variables to illustrate part of what "use strict"
solves. Mistyping variable names can be a common source of errors, where you can have a statement like var color = "blue";
and one thousand lines later you attempt to reassign its value with colour = "red";
. Without strict JavaScript mode, you wouldn't be able to easily detect a statement like colour = "red"
is an undeclared variable, while you were really trying to update the declared variable var color
.
If you declare a "use strict"
statement at the top of a .js
file or function, it ensures only operations on properly declared variables are allowed. Listing 3-13 shows an updated version of listing 3-5 with "use strict"
.
Listing 3-13. "use strict"
to detect undeclared variables
"use strict"; var letter; console.log(letter); function testing() { console.log(number); var number = 2; console.log(number); } testing(); console.log(vowels); var vowels = ["a","e","i","o","u"]; console.log(vowels); random = 8; console.log(random);
Notice listing 3-13 generates an error in the form '<variable> is not defined'. In this case, the random = 8
statement is the problem because it's an undeclared variable. It's also worth pointing out that hoisting still takes place with "use strict"
, since it's an inherent behavior of var
statements, therefore it's still possible to reference variables ahead of their definitions without error, as it's shown with the various console.log
statements in listing 3-13.
Because "use strict"
is a pre-ES6 (ES2015) statement, JavaScript constructs that were added in ECMAScript ES6 (ES2015) or later versions are implicitly strict, such is the case for things like JavaScript classes and modules. Therefore, don't expect to encounter the "use strict"
statement everywhere, since it's redundant in certain circumstances. Still to this day, the "use strict"
statement can be of help to catch undeclared variables like it's shown in listing 3-13, as well as other obscure errors that are otherwise silent (e.g. raise errors when function parameters aren't unique, raise errors when assigning values to references that can't be updated, etc). Where pertinent in the upcoming discussions, I'll reference the "use strict"
statement when it influences the behavior of certain constructs.
Tip You can use a command line tool like Node JS syntax checker to validate JavaScript and not have to load things directly in a browser to detect errors in the console.
The try
, catch
and finally
keywords
In circumstances where you know there's a possibility JavaScript logic can generate errors, it's a best practice to wrap this logic in what's known as a try/catch block. The try
and catch
keywords are used in conjunction with curly brackets { }
to encapsulate logic that should run as an initial block, that in case an error surfaces in any part of the initial block a separate catch block is run, otherwise, if no error is raised in the initial block the separate catch block is ignored. In addition to try/catch blocks, JavaScript also supports try/catch/finally blocks. The purpose of the finally
keyword is to define a third block to complement try
and catch
blocks, so that irrespective of the outcome of said blocks, the instructions in the finally block are always run.
The typical scenario for try/catch and try/catch/finally blocks is in JavaScript logic that involves networking related operation, since network bound logic tends to have a higher degree of uncertainty. Nevertheless, try/catch or try/catch/finally blocks can be used anywhere where it's deemed beneficial, as illustrated in listing 3-14.
Listing 3-14. try/catch/finally
blocks for error handling
var vowels = ["a","e","i","o","u"]; try { console.log("Vowels is %s", vowels); // Access undefined reference console.log("Number is %d", number); } catch (error) { console.error("Error is %s : %s", error.name, error.message); console.trace(error); } finally { console.log("Running finally block"); }
Notice listing 3-14 attempts to access the undefined number
reference, but since the logic is wrapped in a try/catch
block, the workflow isn't completly interrupted and is instead handled inside the catch
block. In this case, the catch
block uses the optional argument (error)
to gain access to the error and its details through the error
reference. Once inside the catch
block the error details in error
are output with a console.error()
and console.trace()
methods. Next, you can see the finally
block represents logic that always runs whether an error is raised or not.
One important aspect of try/catch and try/catch/finally blocks -- shown in listing 3-14 -- is that in case an error ocurrs, the error is placed in a JavaScript error object. Notice in the catch
block, the error
reference outputs the name
and message
properties which belong to the JavaScript error object. The next chapter on JavaScript data types contains more details about JavaScript error objects.
The throw
keyword
The throw
keyword is used to control the type of error that's generated inside a try/catch
block. In listing 3-14 you can see the generated error is a ReferenceError
type, which is a built-in error object. But what happens if you want to generate a custom error for a given condition ? Such as, "input must be a number" or "number can't be greater than 100" ? This is where the throw
keyword can help, as illustrated in listing 3-15.
Listing 3-15. throw
for custom error handling
function testing(number) { try { console.log("Number is %d", number); if (isNaN(number)) { throw "Input must be a number, can't be NaN"; } if (number > 100) { throw new Error("Number can't be greater than 100"); } console.log("Number %d passed validation", number); } catch (error) { console.error("Error is %s : %s", error.name, error.message); console.trace(error); } } testing(); testing(200); testing(50);
Listing 3-15 defines a try/catch block inside the body of the testing
function to validate its number
input. The first call to testing()
doesn't provide a value, therefore the number
value is undefined
. When this happens the function matches the first conditional if (isNaN(number))
that uses the built-in isNan()
function to validate if a value is not a number. You can see that when the number
value is not a number, the throw "Input must be a number, can't be NaN";
statement is run, which raises an error and turns control to the catch block.
Although the throw "Input must be a number, can't be NaN";
is perfectly valid, notice the output in the catch block shows undefined
values for both the error.name
and error.message
properties. The reason for this behavior in this case, is the error generated with throw
is a plain string (i.e. "Input...be NaN") with no name
or message
properties, therefore the output defaults to undefined
values for unknown properties.
The second call to testing()
in listing 3-15 provides an input value of 200
which causes that function to match the second conditional if (number > 100)
when a value is greater than 100. You can see that when the number
value is greater than 100, the new Error("Number can't be greater than 100")
statement is run, which raises an error and turns control to the catch block. In this case, because the error generated with throw
is an Error
object, the output in the catch block for the error.name
and error.message
properties is appropriately output: Error
as the name of the error object and "Number can't be greater than 100"
as the message of the error object. Once again to avoid getting sidetracked, I'll refer you to the next chapter on JavaScript data types which contains more details about these type of JavaScript error objects.
Finally, the third call to testing()
in listing 3-15 provides an input value of 50
which causes the function to run all the way through to its last statement console.log("Number %d passed validation", number)
Namespaces and block scoping, the problem
Let's first take a look at the concept of JavaScript namespaces and the idea of having a function with a fairly common name like render()
. The problem with using a fairly common JavaScript function name -- or variable name for that matter -- is it has the potential to clash with other equally named ones, due to JavaScript's hoisting behavior shown in listing 3-5 and listing 3-7.
Suppose you found two great JavaScript libraries on the web -- named fantastic.js
& wonderful.js
-- and you want to leverage them along with your own amazing.js
library. After you put together your amazing.js
library, you then proceed to make use of all three libraries in the following manner:
Listing 3-16. Naming conflict due to lack of namespace
<script src="fantastic.js"></script> <script src="wonderful.js"></script> <script src="amazing.js"></script> <script> // Let's call the render() function // What happens if there's a render() function in fantastic.js, wonderful.js and amazing.js ? render(); </script>
As you can see in listing 3-16, if all libraries define a render()
function, there's no way to call the render()
functions in each library, unless they use namespaces. This behavior is due to hoisting, which causes all library functions and variables to be hoisted and become part of the same scope (e.g. three global render()
functions, where the last one becomes the definitive one).
This same namespace conflict can happen in .js
files that grow extremely large. In a .js
file with dozens of lines created by a single person it's easy to keep track of function and variable names, but if a file grows to hundreds of lines or is modified by multiple people, it's easy to inadvertently introduce the same name for a function or variable, which leads to name clashes when hoisting is applied. To avoid these naming conflict behaviors you can use namespaces, which is where immediately-invoked function expressions (IIFE) becomes an auxiliary JavaScript namespace artifact.
Now let's take a look at another JavaScript concept that's influenced by hoisting behavior just like namespaces: block scoping. When I first mentioned JavaScript scoping and hoisting, I described how variables can belong to either a global or local scope (i.e. everything in the top-level gets hoisted to the global scope and everything else gets hoisted to their relative local scope). However, having only a global scope and local/function scope can limit the ability to introduce functionality that requires a block scope.
A block scope is a more granular kind of scope than a local/function scope, which is often required in the context of execution blocks, namely that of if
conditionals or for
loops. The following example illustrates the lack of block scoping in JavaScript.
Listing 3-17. Lack of block scoping
var primeNumbers = [2,3,5,7,11]; for (var i = 0; i < primeNumbers.length; i++) { console.log(primeNumbers[i]); } console.log("The value of i is " + i); // i is 5! Leaked from the loop // Let's try curly brackets { for (var j = 0; j < primeNumbers.length; j++) { console.log(primeNumbers[j]); } } console.log("The value of j is " + j); // j is still 5! , simple curly brackets still leak from block scope
The first for
loop in listing 3-17 uses the i
variable as the loop counter. In most programming languages, the norm for anything that happens inside a for loop is to operate within a block scope, that is, statements occurring inside a for loop are confined to the scope of the loop. However, you can see that even though the i
variable is declared as part of the loop, it's accessible even after the loop has ended! This happens because the var i
declaration is hoisted, making the variable accessible long after the loop has ended. In other words, JavaScript var
statements don't support block scope because they're hoisted to their nearest level scope (i.e. global or local/function scope).
The second for
loop in listing 3-17 uses the j
variable as the loop counter and attempts to use an additional set of curly brackets to define a block scope. However, you can see the additional set of curly brackets around the for
loop work to no avail, since the j
variable is still leaked beyond the scope of the for
block.
This lack of support for block scope in var
statements is also where immediately-invoked function expressions (IIFE) become an auxiliary JavaScript block scoping artifact.
Immediately-invoked function expressions (IIFE): Namespaces and block scoping solved, the early years
The past section introduced two problems with JavaScript hoisting: It lifts functions to the same scope, causing potential namespace conflicts if functions with the same name are accessed from multiple .js
files and it also lifts variables to their nearest level scope lacking any type of block scoping. Both problems can be solved with Immediately-invoked function expressions (IIFE), a specialized type of function expression, the last of which was presented in the first class functions: Function declarations, function expressions, hoisting & undefined section.
IIFE can be one of the most awkward pieces of syntax in JavaScript because they can visually appear to be doing nothing, as a function expression wrapped around parenthesis (e.g.(function testing(){ }());
. The purpose of this wrapper is to immediately trigger the enclosing function logic (i.e. it isn't called explicitly) and the reason for doing this is almost always to simulate namespaces & block scoping.
Listing 3-18 illustrates an updated version of listing 3-16 making use of IIFE to support namespaces.
Listing 3-18. Namespaces with IIFE prevent naming conflicts
// Contents of fantastic.js var fantasticNS = {}; (function(namespace) { namespace.render = function() { console.log("Hello from fantasticNS.render()!") }; })(fantasticNS); // Contents of wonderful.js var wonderfulNS = {}; (function() { this.render = function() { console.log("Hello from wonderfulNS.render()!") }; }).apply(wonderfulNS); // Contents of amazing.js var amazingNS = {}; (function() { var privateRender = function() { console.log("Hello from amazingNS.render()!") }; this.render = function() { privateRender() }; }).call(amazingNS); // Let's call the render() function for each of the different libraries fantasticNS.render(); wonderfulNS.render(); amazingNS.privateRenderer; // This does nothing because privateRenderer is local to IIFE block amazingNS.render();
The contents of fantastic.js
in listing 3-18 first show the fantasticNS
variable is assigned to an empty object {}
, that will serve as the namespace reference -- for the moment, bear with me on the talk about JavaScript objects, the next chapters on JavaScript data types and JavaScript object-orientated and prototype-based programming contain more details about this topic. Next, notice the IIFE syntax (function() {} );
which wraps the bulk of the logic, including a render
function. In this case, the IIFE is given the fantasticNS
reference, to permit access to the functions inside the IIFE.
The contents of wonderful.js
in listing 3-18 use a slightly different syntax variation to incorporate a namespace. Similarly, the wonderfulJS
variable is assigned to an empty object {}
that will serve as the namespace reference. However, notice the IIFE syntax now uses (function() { }).apply()
to wrap the bulk of the logic, including a render
function. In this case, the apply()
[10] function allows IIFE to modify the function's this
reference to another value -- in this case to wonderfulNS
-- and forgo using arguments in its definition (e.g.(function(namespace) { })
like the IIFE for fantastic.js
).
The contents of amazing.js
in listing 3-18 use the same namespace technique as wonderful.js
, but instead use the call()
[11] function to produce the same outcome. In addition, notice the body of the amazing
IIFE contains the statement var privateRender = function() {}
. By using var
, the privateRender
function expression becomes confined to the IIFE and inaccessible outside of its scope -- which is called a block scope -- even though the IIFE does make use of namespace to access other functions like render
.
Finally in listing 3-18, you can see the calls to the various render()
functions relying on the different namespaces. In addition, you can also see that attempting to access amazingNS.privateRenderer
doesn't produce any output, because privateRenderer
is a var
confined to the scope of the IIFE.
Now that you've learned how IIFE support namespaces, let's take a look at how IIFE can support block scoping. Listing 3-19 illustrates an updated version of listing 3-17 making use of IIFE to support block scoping.
Listing 3-19. Block scoping with IIFE
var primeNumbers = [2,3,5,7,11]; for (var i = 0; i < primeNumbers.length; i++) { console.log(primeNumbers[i]); } console.log("The value of i is " + i); // i is 5! Leaked from the loop // Let's try curly brackets { for (var j = 0; j < primeNumbers.length; j++) { console.log(primeNumbers[j]); } } console.log("The value of j is " + j); // j is still 5! , simple curly brackets still leak from block scope // Let's try an IIFE (function() { for (var k = 0; k < primeNumbers.length; k++) { console.log(primeNumbers[k]); } })(); console.log("The value of k is " + k); // k is not defined ReferenceError, k is out of scope by using IIFE
The first two examples in listing 3-19 are the same ones presented in listing 3-17 that leak a loop's variables i
and j
beyond the scope of the for
block.
The third for
loop in listing 3-19 illustrates how to obtain block scoping by means of an IIFE. You can see that by wrapping the for
loop in plain IIFE syntax (i.e. (function() { })()
, with no namespace), the k
variable used by the for
loop remains contained to the block scope, with any attempt to access it outside the loop generating a reference error. It's worth pointing out, this IIFE block scope behavior is the same one illustrated in the previous IIFE example in listing 3-18 when a call is made to amazingNS.privateRenderer
, the privateRenderer
is inaccessible because it's contained to the block scope of the IIFE.
The key takeaways you should get from these IIFE examples and JavaScript functions in general is:
- A JavaScript
function()
always has its own scope and maintains its context in isolation, as illustrated in listing 3-19. - A JavaScript
function()
can always access its context through thethis
keyword, as illustrated in listing listing 3-18. - A JavaScript
function()
can always modify its default context (i.e.this
) with an outside context reference using a function likeapply()
orcall()
, as illustrated in listing listing 3-18
Tip IIFE can also be declared with thevoid
operator. See thevoid
keyword and listing 3-31 for additional details.
Object assignments and object notation: Namespaces solved another way, the early years
Even though IIFE are a common practice to create JavaScript namespaces, there are other ways to create them. For the sake of completeness, other variations to support JavaScript namespaces, include: objects assignment and object notation in conjunction with plain function expressions and IIFE.
Listing 3-20. Namespaces with object assignment and object notation
var fantastic = { } fantastic.render = function() { console.log("Hello from fantastic.render()!") } var wonderful = { render: function() { console.log("Hello from wonderful.render()!"); } } var amazing = (function () { return { render: function() { console.log("Hello from amazing.render()!"); } } })(); fantastic.render(); wonderful.render(); amazing.render();
The first example in listing 3-20 creates the empty fantastic
object reference and then directly assigns the render
function to it. This direct assignment namespace technique is valid due to the flexibility of JavaScript objects, although it can become sloppy because namespace definition can be spread out in multiple places.
The second example consists of creating the wonderful
object literal [12] -- which is simply a comma-separated list of name-value pairs wrapped in curly braces. Where as the third example consists of creating the amazing
IIFE which returns an object literal like the second example.
Toward the end of the example, you can see how all three different namespace syntax variations are capable of invoking a function named render()
without interfering with one another.
Why are techniques to support JavaScript namespaces so convoluted and fragmented ?
At the beginning of Modern JavaScript essentials, I mentioned how JavaScript modules, namespaces and modules types have always been one of the language's achilles heel. Well now you've seen this first hand, exploring how you need to make use of IIFE, object data types and function()
statements to obtain namespace behavior.
The reason JavaScript namespaces are so convoluted and fragmented is because in the early years of the language there wasn't any formal support for JavaScript namespaces. ECMASCript 5 simply relied on ad-hoc mechanisms to simulate namespaces like you just learned in the past sections. ECMAScript 6 (ES2015) solved this problem by formally introducing syntax for namespaces that you'll learn shortly.
Lexical scope and the this
execution context
JavaScript is a lexically scoped language, which means its statements are resolved based on where they're declared. And now that you've learned how JavaScript works with a global scope, local scope and can also support block scope, understanding lexical scope -- or static scope as it's also known -- is relatively easy.
JavaScript always attempts to resolve its statements using the scope where they're declared and moves upward in scope until it's able to resolve a statement -- with block scope being the inner most possible scope, followed by local scope and finishing with the global scope. This behavior is illustrated in listing 3-4, where the letter
statement can't be resolved within its local testing()
function scope and is instead resolved with the global scope statement var letter = "a";
.
By the same token, being lexically scoped also means a JavaScript statement declared in an inner scope isn't visible in an outer scope -- unless it's also declared in the outer scope. This behavior is illustrated in listing 3-19, where the k
reference can't be resolved in the global scope because it's enclosed and resolved in the local function (block) IIFE scope.
JavaScript scope is closely tied to another concept called the execution context -- or simply context -- that's managed through the this
keyword. In listing 3-18 you learned how a function()
's default context can be altered and assigned properties through the this
keyword. Although I'll once again talk about JavaScript objects prematurely, it's essential to do so in order to grasp the concept of JavaScript context and closures. If you want to explore JavaScript objects in-depth now, read the next chapters on JavaScript data types and JavaScript object-orientated and prototype-based programming.
In very simple terms, you can assume all JavaScript scopes have their own context or this
reference. In reality though, it's not the scope per se that provides access to a context or this
reference, it's the underlying object that does. In OOP (Object Orientated Programming) the this
keyword is a standard to self-reference an object instance and in JavaScript a function()
is itself an object. Therefore, because a function()
always creates its own scope and is also an object, every scope has its own context or this
reference.
Now let's take a closer look at the JavaScript context or this
reference. Not only does every JavaScript scope or technically object have its own default this
context that can be modified -- as you learned in listing 3-18 -- the JavaScript context or this
reference can also vary depending on the lexical scope. Since all you've seen up to this point are function()
objects, listing 3-21 illustrates how the this
reference or context can vary depending on the lexical scope of a function()
object.
Listing 3-21. Execution context or this
varies depending on lexical scope
var message = "Good day!"; var morning = { message: "Good morning!" } var afternoon = { message: "Good afternoon!" } var evening = { message: "Good evening!" } var greeter = function() { // this always varies depending on calling context var message = "Good night!"; return this.message; } // Call to greeter() // uses this.message=(global)message since global is the calling context console.log(greeter()); // Good day! // Call to greeter()) modifies calling context with bind() // modifies this.message=morning.message since morning is the calling context console.log(greeter.bind(morning)()); // Good morning! // modifies this.message=afternoon.message since afternoon is the calling context console.log(greeter.bind(afternoon)()); // Good afternoon! // modifies this.message=evening.message since evening is the calling context console.log(greeter.bind(evening)()); // Good evening! var strictGreeter = function() { "use strict"; var message = "Good night!"; // "use strict" forces 'this.message' to be in local scope/context return this.message; } // Call to strictGreeter() which uses "use strict" // error because this and this.message are undefined in local context console.log(strictGreeter()); // Cannot read property 'message' of undefined
Let's start with the greeter()
function in listing 3-21 which declares a local message
variable and returns this.message
. Notice how the first call made to the greeter()
function outputs "Good day!"
due to lexical scoping behavior. Because the greeter()
function scope can't resolve the this.message
statement -- only a local var message
variable which is unrelated to the this
context -- lexical scoping makes JavaScript look outward to the next scope to resolve the this.message
reference. It's in the next scope -- the global scope -- where JavaScript resolves this.message
to "Good day!"
due to the var message = "Good day!";
statement on the first line.
Now you may be asking yourself, why is the global var message = "Good day!";
assigned to this.message
in the global scope, but the similar var message = "Good night!";
statement ignored for assignment in the this.message
function scope ? As it turns out, every JavaScript statement is eventually assigned to its context, so the global var message = "Good day!";
statement ends up being treated as is if it were declared as this.message = "Good day!";
-- a perfectly valid syntax which you can use explicitly by the way. So does this mean there's an implicit global this
context reference ? Exactly, all globally scoped statements end up assigned to the top level this
context which is called the JavaScript global object.
The JavaScript global object and the this
, global
and window
keywords
In addition to all the JavaScript scoping and context behaviors you've learned up to this point, there's a closely related concept to scoping and context dubbed the JavaScript global object. In listing 3-21, the standard this
keyword is used to reference the top level context, however, the JavaScript global object or top level context is also often referenced with the global
or window
keywords.
Because the JavaScript global object inevitably requires some prior knowledge on JavaScript data types, the use of the this
, global
and window
keywords for top level context is explained in the global object section of the JavaScript data types chapter.
Continuing with the example in listing 3-21, there are three more calls made to the greeter()
function, but notice they're all prefixed with the bind()
[13] function. The bind()
function solves the potential unintended side-effects of JavaScript lexical scoping on the context/this
reference, creating a new function with a specific context reference.
Therefore due to the bind()
function, the greeter.bind(morning)()
call outputs "Good morning!"
because the morning
reference is composed of var morning = { message: "Good morning!" }
, a JavaScript object where morning
gets treated as this
and this.message
is assigned the "Good morning!"
value. A similar binding ocurrs for the greeter.bind(afternoon)())
and greeter.bind(evening)()
calls which output "Good afternoon!"
and "Good evening!"
, respectively, due to the contents of the afternoon
and evening
objects.
What is the difference between the apply()
, call()
and bind()
functions that alter the this
context ?
The apply()
and call()
functions used in listing 3-18 are designed to immediately call a function with a modified this
context. Both the apply()
and call()
functions produce identical results, the only difference is their syntax, the call()
function accepts an optional list of arguments (e.g..call(this,arg1,arg2)
) and the apply()
function accepts an optional array of arguments (e.g..apply(this,[arg1,arg2])
).
The bind()
function used in listing 3-21 is designed to create a new function with a modified this
context, in order to have a permanently modified function with another this
context vs. constantly modifying the this
context on every call like it's done with apply()
and call()
.
Eventhough the bind()
function is helpful to unequivocally assign a specific this
context to a JavaScript object -- a function()
object in listing 3-21 -- it still leaves the default lexically scoped behavior of this
. It's one thing for an explicitly declared global JavaScript variable like var letter = "a";
in listing 3-4 to be accessed inside the scope of a function, it's quite another for an implicit global this
reference to be accesible inside a function, when it could well be the function's own this
reference you're attempting to access.
In the final part of listing 3-21 you can see the strictGreeter()
function is almost identical to the greeter()
function, except it uses the "use strict";
statement. If you recall from the "use strict"
section earlier, by adding the "use strict";
statement to the beginning of a .js
file or function, the JavaScript engine enforces a set of stricter syntax rules, one which in this case is generating an error when an attempt is made to access the this
reference in a scope where it isn't defined. Toward the end of listing 3-21, you can see that attemtping to call the strictGreeter()
function generates an error because this.message
isn't defined as part of the function's context and lexical scoping from the this
context of the global scope isn't applied because there's no explicit this
declaration in the JavaScript global object.
Having this potential of multiple this
context references clashing with one another -- because one this
is generated for every scope/object -- creates an edge case. What happens if you want to access the this
context of an outer scope in an inner scope ? How can you tell them apart if they both use the this
reference ? This is solved in one of two ways, you can create a placeholder reference to access one this
context with another name (e.g. var that = this
) or you can use the apply()
or call()
functions to use the same this
context across different scopes/objects. Listing 3-22 illustrates both approaches.
Listing 3-22. Access the same this
context in different scopes/objects
var tableTennis = {} tableTennis.counter = 0; tableTennis.play = function() { // 'this' is the tableTennis object in this scope // Use placeholder 'that' to access 'this' in inner functions var that = this; var swing = function() { // 'this' is the local function object in this scope // must use 'that' to access outer scope that.counter++; } var ping = function() { // 'this' is the local function object in this scope // must use 'that' to access outer scope console.log("Ping " + that.counter); } var pong = function() { // 'this' is the local function object in this scope // must use 'that' to access outer scope console.log("Pong " + that.counter); } // Call inner functions in sequence swing(); ping(); pong(); } // Call tableTennis.play() three times tableTennis.play(); tableTennis.play(); tableTennis.play(); tableTennis.playApply = function() { // 'this' is the tableTennis object in this scope var swing = function() { // Use this local function object, must use apply() on call to change 'this' this.counter++; } var ping = function() { // Use this local function object, must use apply() on call to change 'this' console.log("Ping " + this.counter); } var pong = function() { // Use this local function object, must use apply() on call to change 'this' console.log("Pong " + this.counter); } // Call inner functions in sequence // with apply() so 'this'(tableTennis object) is visible inside inner functions swing.apply(this); ping.apply(this); pong.apply(this); } // Reset counter tableTennis.counter = 0; // Call tableTennis.playApply() three times tableTennis.playApply(); tableTennis.playApply(); tableTennis.playApply();
Listing 3-22 begins with the empty tableTennis
object and adds the counter
and play
properties to it. Notice how the play
property is a function()
which in itself has other function()
statatements that are invoked when play
is called. Because each function()
has its own this
context, the play
function relies on the var that = this
statement to use the that
reference in the inner swing()
, ping()
and pong()
functions to get a hold of the play
function context.
Careful reassigning this
to potentially conflicting keywords
Listing 3-22 uses the var that = this
statement to allow access to one this
context with the that
reference. While it's technically possible to use any reference (e.g. foo
, bar
) for this purpose, you should be careful in selecting this reference.
For example, you may encounter many online examples that use the var self = this
statement to reassign this
, while this can appear to be harmless, the window
reference (i.e. the JavaScript global object in certain environments) has the self
property. And because the window
reference is also accessible through this
, a statement like var self = this
can cause unintended behaviors for window.self
.
The other alternative to access an outer scope this
context presented in listing 3-22 is through the apply()
function -- although technically call()
produces the same outcome as apply()
and could have been used instead.
In listing 3-22 you can see the playApply
property is added to the tableTennis
object and that its contents are a similar function to the play()
function. However, notice the inner swing()
, ping()
and pong()
functions of the playApply()
function use the this
reference directly and get the expected value, how is this possible ? By calling each inner function with a modified this
context, which corresponds to the outer this
context. Notice the calls to each inner function are swing.apply(this);
, ping.apply(this);
and pong.apply(this);
and because this
at this point corresponds to the outer this
context, it gets used as the this
of each inner function.
Closures: Functions that remember their lexical scope
Now let's explore closures which are better known for their special lexical scope behavior. Under most circumstances, calls made to a function()
produce immediate outcomes. That is to say, when a function executes, it runs its logic -- using input arguments or not -- and produces a result: it returns "yes" or "no", it generates an error, it returns data or does whatever else the function is designed to do. However, there can be circumstances when a call made to function has to wait until another action is fulfilled before the function is able to complete its duties.
With JavaScript conceived for browsers and UI (User Interface) type programming, this scenario of calling a function and it being dependant on the result of another action to finish is quite common. For example, functions associated with web page events are always dependent on user actions to finish their execution (e.g. a mouse click to trigger a pop-up or a mouse hover to change an image). Similarly AJAX functions are always dependent on remote services to provide additional input or data to finish their execution.
So what is the big deal about a function having to wait for another action to complete ? The biggest issue is related to how a JavaScript function()
operates with lexical scope and how it manages the different scopes when multiple functions are interacting with one another. Listing 3-23 illustrates this behavior with two closure examples.
Listing 3-23. Closures enclose their lexical scope
var countClosure = function() { // local variable var counter = 1; // return function to be called after termination 'encloses' lexical scope return function () { console.log(counter); // var counter is accesible counter += 1; // counter can be incremented and persists in outer scope } }; // Call to countClosure function var mycounter = countClosure(); // countClosure function is done, but still invoked to trigger its return function mycounter(); mycounter(); mycounter(); var buttonMaker = function(value) { // local variable assigned input argument var name = value; // return functions to be called after termination 'encloses' lexical scope return { name: function() { console.log("Button name is " + name); // var name is accesible }, click : function() { console.log("Clicked on " + name); // var name is accesible }, hover : function() { console.log("Hovered over " + name); // var name is accesible } } } // Call to buttonMaker function with different input values var redBtn = buttonMaker("Red"); var yellowBtn = buttonMaker("Yellow"); var blueBtn = buttonMaker("Blue"); // buttonMaker function is done, but can still return different results // note the following function calls on buttonMaker have access to the // var 'name' in buttonMaker, even though the buttonMaker function is done // This is because all lexically scoped variables are 'enclosed' with the // return result, hence the name 'closure' redBtn.name(); redBtn.click(); yellowBtn.click(); blueBtn.click(); redBtn.hover(); yellowBtn.hover();
Listing 3-23 begins with the countClosure()
function which returns another function()
as its result. This behavior is a more elaborate example of the one in listing 3-6 that illustrated how JavaScript can treat functions as values, except in this case, it returns a yet to be evaluated function!
Notice the countClosure()
function declares var counter = 1;
and immediately after returns a function
that outputs a log statement and increases the counter
variable. What's interesting about this syntax is the ability of the return
function to access a variable in the outer scope. Notice the var mycounter = countClosure();
statement in listing 3-23 makes a call to countClosure()
and assigns the result to mycounter
, at this juncture, the countClosure()
function is done but its return
function has yet to be evaluated.
Next, the mycounter
reference is evaluated as a function -- adding ()
-- which triggers the actual logic inside the return
function. This action is performed three times, but more importantly, notice how the logic of the return
function is capable of accessing and updating the counter
variable in the outer scope, even though the outer scope function (i.e. countClosure()
) has apparently finished. The reason JavaScript is able to access outer scope variables when it returns a full-fledged function()
is because it 'encloses' the lexical scope and it can therefore gain access to variables in the outside scope, hence the name closure.
The second example in listing 3-23 is the buttonMaker()
function which uses more complex closure logic. The buttonMaker()
function accepts a single value
argument which is then assigned to var name = value;
. However, unlike the countClosure()
function which returns a single function()
, the buttonMaker()
function returns an object with multiple functions. Here it's important not to loose sight of the fact the various return
functions all make use of the outer scope name
variable.
Next, three calls are made to the buttonMaker()
function using diffrent arguments, with each result stored in one of three references: redBtn
, yellowBtn
and blueBtn
. At this juncture, the buttonMaker()
function is done but its return
object functions have yet to be evaluated. Next, calls are made to the various return
object functions in each of the three buttonMaker()
function references. Here again, the critical aspect of this functionality is that the various return
object functions are capable of accessing the name
variable in the outer scope because the lexical scope is 'enclosed'.
The let
and const
keywords: Block scoping solved, the modern years; plus saner access, defaults & temporal dead zones (TDZ) for variables
In order to support block scoping, you learned in listing 3-19 how to use IIFE syntax (e.g. (function() { })()
) to avoid leaking values to outer scopes. With the introduction of ES6 (ES2015), a more natural syntax was introduced to support block scoping.
Listing 3-24 illustrates the use of the let
keyword and how it supports block scoping in a more natural manner.
Listing 3-24.let
block scoping
let vowels= ["a","e","i","o","u"]; for (let i = 0;i < vowels.length; i++) { console.log(vowels[i]); } // Let's see if we can access the i loop variable... console.log(i); // ReferenceError: i is not defined
Notice the for
loop in listing 3-24 uses the i
variable as the loop counter, but more importantly, notice how the i
variable is preceded by the let
keyword vs. the var
keyword typically used to declare variables. As you can see in this example, attempting to access the i
variable after the loop scope results in an error. This means the let
keyword naturally supports block scoping, foregoing the need to use ad-hoc block scope syntax with IIFE, something that also reduces the complexity of creating block-scoped logic (e.g. nested loops).
In addition to supporting block scoping, the let
keyword also introduces additional safeguards to avoid problems that can surface with the var
keyword, such as duplicate variable definitions and function parameter name clashes, which are illustrated in listing 3-25.
Listing 3-25. let
vs. var
duplicate and function parameter name clashes
// THIS IS VALID WITH var... // THIS IS INVALID WITH let var number = 1; let number = 1; // 500 lines later // 500 lines later // var with same name redeclared is valid // let with same name redeclared is an error var number = 2; let number = 2; // Duplicate declaration "number" var echoer = function(message) { let echoer = function(message) { // var with argument name is valid // Reusing function argument name as let is an error // gets overwritten // Duplicate declaration "message" var message = "Local message"; let message = "Local message"; console.log(message); console.log(message); return message; return message; } } echoer("Hello there!"); echoer("Hello there!");
Notice the number
reference is declared twice in listing 3-25, if you use a var
statement JavaScript doesn't mind the duplicity, however, if you use a let
statement JavaScript generates an error. Similarly, notice the echoer()
function uses the message
reference as both an input argument and a local variable, with a var
statement JavaScript ignores this potential pitfall, but with a let
statement JavaScript generates an error to warn you of the potential conflict.
The const
keyword has similar behaviors to let
block-scoped variables, except const
values can't be updated. Once a const
statement is defined its value is constant and attempting to modify it generates an error, as illustrated in listing 3-26.
Listing 3-26. const
vs. let
behaviors
// THIS IS VALID WITH let... // THIS IS INVALID WITH const let number = 1; const number = 1; // let can be reassigned // const can't be reassigned number = 2; number = 2; // Assignment to constant variable // let can be left undefined // const can't be left undefined let letter; const letter; // Unexpected token
As you can see in listing 3-26, const
statements provide an additional safeguard to prevent variables from having their values reassigned or being left as undefined
.
Finally, another important aspect of let
and const
statements has to do with hoisting, undeclared and undefined behaviors. Back in listing 3-5, you learned how var
statements get hoisted to the top of their scope, allowing you to inadvertently access them, on top of which JavaScript assigned such statements an undefined
primitive data type until the actual statement declarations were reached.
These undesirable behaviors, of accessing variables before they're declared and automatically assigning them an undefined
primitive data type before their declarations are reached, aren't permitted in let
and const
statements. In other words, if you attempt to access let
or const
statement references before their declaration, you'll get a // Reference error: <let_or_const_name> is not defined
. This let
and const
statement behavior is much safer and more in-line with most programming languages, than JavaScript's var
behavior.
For the sake of accuracy, this behavior of let
and const
statements might lead you to believe that hoisting doesn't apply to these type of statements, but in fact hoisting is applied to let
and const
declarations. The only thing let
and const
statements do differently, is they don't allow access to such statements until their declaration is reached and it also doesn't give such statement an automatic undefined
value before their declaration is reached.
The ES6 (ES2015) specification confirms this behavior: "The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable's LexicalBinding is evaluated"[14]. In friendlier terms, "the variables are created when their containing Lexical Environment is instantiated" means variables are created as soon as they enter their scope -- which indicates hoisting behavior -- "but may not be accessed in any way until the variable's LexicalBinding is evaluated" means they cannot be accessed until their declaration is reached.
The act or error of attempting to access a let
or const
variable before its declaration is reached, is said to happen because the variable is in a temporal dead zone or TDZ -- a fancy name indicating the variable has been created (due to hoisting), but is still 'temporarily dead' because its declaration hasn't been reached.
Use let
and const
over var
Because let
and const
statements provide block-scoping, in addition to other safeguards just mentioned in the previous section, they should always be preferred over JavaScript var
statements.
Although var
statements continue to be valid in JavaScript, the only reason you should even consider using var
statements is if you want to access a reference globally (i.e. across all scopes), since it's the only option available. Using let
and const
statements force you to create cleaner and more thought-out JavaScript logic, since statements are restricted to their scope.
The export
& import
keywords and modules: Namespaces solved, the modern years
In order to support namespaces, you learned in listing 3-18 and listing 3-20 how to use either IIFE, object assignments or object notation, to allow functions with the same name to interact with another. With the introduction of ES6 (ES2015), a more natural syntax was introduced to support namespaces.
Because namespaces are closely tied to the concept of classifying entities (e.g. variables, functions) into different groups, JavaScript took a similar approach to other programming languages and introduced modules -- as they're also called in Python or packages as they're known in Java. Modules in JavaScript are supported through the export
and import
keywords, which allow the visibility of entities to be controlled between files and avoid name collisions.
Let's rework the examples from listing 3-18 and listing 3-20 that use namespaces with IIFE, object assignments and object notation, to use JavaScript modules and the export
/import
keywords.
Listing 3-27. Modules with export
and import
to support namespaces
// Contents of fantastic.js export let render = function() { console.log("Hello from fantastic.render()!") } // Contents of wonderful.js export let render = function() { console.log("Hello from wonderful.render()!"); } // Contents of amazing.js export let render = function () { console.log("Hello from amazing.render()!"); } // Contents of script.js import * as fantastic from './fantastic.js'; import * as wonderful from './wonderful.js'; import * as amazing from './amazing.js'; fantastic.render(); wonderful.render(); amazing.render(); // index.html <!DOCTYPE html> <html> <body> <h1>ES6 modules with <script type="module"> - See console for results</h1> <script src="amazing.js" type="module"></script> <script src="fantastic.js" type="module"></script> <script src="wonderful.js" type="module"></script> <script src="script.js" type="module"></script> </body> </html>
Listing 3-27 illustrates three function expressions named render()
placed in three different files, similar to those in listing 3-18. However, notice that in addition to using let
statements (vs. var
statements), the function expressions in listing 3-27 are preceded by the export
keyword. In this case, the export
keyword gives each render()
function expression the ability to be accessed from other files or modules.
Next, in listing 3-27 you can see the contents of a fourth file named script.js
with multiple import
statements. In each case, the import
keyword is followed by the *
symbol or wildcard (to indicate everything), followed by the as
keyword and a namespace identifier, followed by the from
keyword and the name of the JavaScript file with export
statements.
Therefore, the import * as fantastic from './fantastic.js';
statement, indicates to import every exportable entity in the fantastic.js
file and make it available under the fantastic
namespace, a technique that's also used to import the contents of the wonderful.js
and amazing.js
files. Finally, toward the end of listing 3-27, the namespaces for each import
statement are used to invoke the render()
function from each of the three files without threat of name collisions.
Also notice the HTML document in listing 3-27 uses the the HTML <script> element with the type="module"
attribute to tell a browser's JavaScript engine to process the contents of the .js
file as a module, that is, to expect export
/import
statements and process them accordingly.
As you can see, the JavaScript export
/import
module syntax is much simpler and straightforward to use than the namespace techniques in listing 3-18 and listing 3-20. In addition, notice the import
statement also lets the user of a module define the namespace vs. the previous techniques in which namespaces are hard-coded by the creator of a file.
The only caveat to using export
/import
module syntax is it's designed for ES6 (ES2015) JavaScript engines. Although now a days most browsers support JavaScript modules, it can always be the case that some users are stuck on old browsers without this ES6 (2015) feature, in which case this syntax wouldn't work. To solve this issue for users on older browsers, you'd need to create equivalent non-ES6 module logic as a fallback mechanism and add another HTML <script>
element with the nomodule
attribute, a process that's described in greater detail in table 3-1. HTML <script> element attributes.
With the limitations of the export
/import
module syntax to run on browsers out of the way, I'll describe an additional syntax variation for the export
and import
keywords.
Although you can use the export
keyword to precede as many let
, const
, function
-- or inclusively var
-- statements as needed, just as it's shown in listing-3-27, this can lead to excessive typing. To alleviate this problem, the export
keyword can also be used at the end of a file to export multiple constructs in a single line, as illustrated in listing 3-28.
Listing 3-28. Modules with refined and default
, export
and import
// Contents of amazing.js const vowels = ["a","e","i","o","u"]; let render = function () { console.log("Hello from amazing.render()!"); } function testing() { console.log("testing() in amazing.js"); } export {vowels,render,testing}; // Contents of fantastic.js export let render = function() { console.log("Hello from fantastic.render()!") } let main = function() { console.log("main() in fantastic"); } export default main; // Contents of wonderful.js export let render = function() { console.log("Hello from wonderful.render()!"); } console.log("Log statement in global scope of wonderful.js"); // Contents of script.js import coolstuff, * as fantastic from './fantastic.js'; import './wonderful.js'; import {vowels as letters, render,testing} from './amazing.js'; fantastic.render(); console.log(letters); render(); testing(); coolstuff(); // index.html <!DOCTYPE html> <html> <body> <h1>ES6 modules with <script type="module"> - See console for results</h1> <script src="script.js" type="module"></script> </body> </html>
Notice how the contents of the amazing.js
file in listing 3-28 are a const
, let
and function
statements and toward the end is the export {vowels,render,testing};
declaration that makes all three statements available to anyone using the file or module.
The export
keyword can also be used in conjunction with the default
keyword to make a function
expression a file's default export, as it's shown in the contents of the fantastic.js
file. This technique is common to expose a function that executes a key file routine (e.g. main
, core
), so that whomever uses the file can simply use the import
without the need to guess which function to run -- it's worth pointing out that the export
keyword with the default
keyword can't be used with let
, const
or var
statements, only with function
expressions. In listing 3-28 you can see the statement export default main;
makes the main
expression the default function for the fantastic.js
file.
In addition to the import * as <namespace_reference> from '<file_name>';
syntax presented in listing listing-3-27 -- which imports every construct in a file making it accessible under a namespace reference -- the import
keyword also has additional syntax variations, illustrated in the script.js
file in listing 3-28.
To selectively import certain constructs into a file you can use a list of names wrapped in { }
, instead of the *
wildcard symbol, listing 3-28 illustrates the use of these import
syntax variations. For example, to only import the render
function from the fantastic.js
file, you can use the syntax import {render} from 'fantastic.js'
. To use a different reference name than the name used in the export file (i.e. an alias), you can use the as <reference_name>
(e.g. import {render as fantastic_render} from 'fantastic.js'
to use fantastic_render
as the reference). And to import multiple constructs selectively you can use a CSV-type list (e.g. import {render as fantastic_render, number as fantastic_number} from 'fantastic.js'
).
Another variation of the import
keyword is to only declare it with the name of a file to execute the contents of its global scope, as illustrated in the import './wonderful.js';
illustrated in listing 3-28. For example, the import './wonderful.js'
statement executes the global contents of the wonderful.js
file and the rest of its contents remain isolated, in this case, it means the console.log()
statement in the file -- belonging to the global scope -- is executed. Finally, to complement the export default
syntax, the import
keyword can be used in conjunction with a reference followed by the from '<file_name>';
syntax. For example, the import coolstuff from './fantastic.js'
statement would import whatever function is defined with export default
in fantastic.js
and make it accessible through the coolstuff
reference. It's worth pointing out the import
keyword can be combined to import a default function, as well as all other constructs or selective constructs. For example, import coolstuff, * as fantastic from './fantastic.js'
-- used in listing 3-28 -- or import coolstuff, {render as fantastic_render} from './fantastic.js'
.
Pre-ES6 (ES2015) modules detour: CommonJS, AMD and UMD
Now that you know how JavaScript works with standard ECMAScript modules, it's important to take a brief detour to talk about non-ECMAScript module alternatives. Like any standard, adding new features to ECMAScript is a time consuming process that requires committees and a more formal approval process. However, in the real-world, things move much faster. As I mentioned in the modern JavaScript essentials modules, namespaces & module types sub-section, the 4 to 6 year time frame between ES5 and ES6's module syntax (i.e. import
/export
keywords), saw three more variations come to light to support JavaScript modules: CommonJS, AMD and UMD -- some of which are still in use to this day.
In most cases, module loaders and bundlers will save you the need to know these non-ECMAScript module syntax variations. Still, it's important you realize there's more to JavaScript namespaces and modules beyond the official ECMAScript standard.
One of the first use cases for modules in the pre-ES6 age came with the use of JavaScript outside of a browser through Node JS. Unlike browser JavaScript engines that are inherently tied to running something visual, JavaScript engines that operate outside of a browser can demand a much wider range of functionalities (e.g. network operations, database operations, file manipulation operations), similar to other programming language run-times (e.g. Python, Java, PHP). In this sense, the idea of loading dozens or hundreds of different .js
files to support these functionalities into the same context or global object became intractable -- due to the potential amount of name clashes and other limitations already described in the past section -- which is why JavaScript got its first module system called CommonJS[15] built to run on top of ES5.
The CommonJS standard uses the exports
and require
keywords to reference modules. For example, if you encounter the syntax var fantastic = require("fantastic.js")
, this is CommonJS syntax telling the JavaScript engine to load the contents of the fantastic.js
file and make them available under the fantastic
namespace reference. Similarly, if you encounter the exports
keyword followed by a value assignment, it's CommonJS syntax telling the JavaScript engine to expose a file's constructs for access in other files. Although strikingly similar to standard JavaScript ES6 module syntax, the exports
and require
keywords are plain JavaScript references which get their special functionality through the CommonJS library designed to run on an ES5 environment.
Because CommonJS was the first approach to grant JavaScript the ability to run the closest thing to a module system, it's common to still find many JavaScript projects with exports
and require
statements, specifically those reliant on Node JS which in itself is a CommonJS based system.
Although CommonJS was a step in the right direction for module support, loading dozens or hundreds of .js
files represented another problem. As mentioned in the the HTML <script> element section, by default JavaScript loads .js
files in a sequential manner, meaning that all files are loaded one after the other before any actual work gets done. This in turn creates a bottleneck -- particularly for visually bound applications -- requiring the loading of modules to take up an application's entire lead up time. This paved the way for a second non-ECMASCript module system named AMD (Asynchronous Module Definition)[16].
What AMD solved was the ability to load JavaScript modules asynchronously (i.e. without blocking), allowing module files to be loaded discretely as they were needed, without hoarding the entire lead up time (e.g. initially loading only essential modules, followed by application work, followed by more module loading as needed, followed by more application work, and so on). To achieve this, AMD relies on the special syntax define(id?, dependencies?, factory);
, where id
represents a module identifier, dependencies
a list of module identifiers that need to be loaded first (in order for the module to work) and factory
represents a function to execute for the instantiation of the module.
Similar to CommonJS, what gives the AMD define()
syntax its special functionality is the AMD library designed to run on an ES5 environment. For the most part, due to AMD's more limited scope you're less likely to encounter its syntax in JavaScript projects, but if you do, it's generally as part of a larger JavaScript project -- put together by its creators -- to ensure the efficient loading of modules (e.g. the JavaScript DOJO toolkit uses AMD modules [17]).
Because AMD's define
syntax complements CommonJS's exports
and require
syntax, a third module system combining the features of both AMD and CommonJS emerged: UMD (Universal Module Definition)[18]. Fortunately, by the time UMD became a reality, the stadard ECMASCript module system (i.e. import
/export
keywords) was well underway offering not only what CommonJS, AMD and UMD achieved, but also solving the issue of module circular dependencies. For this reason, UMD is the least likely non-ECMAScript module syntax you're bound to encounter, so I'll leave the conversation on UMD at that.
But as you can see, there are actually three non-ECMAScript JavaScript module variations that filled the void for modules up until standard JavaScript modules came along in ES6 (ES2015). All of which begs the question, how can you easily work with this many JavaScript module and syntax variations in a sane manner ? The process as it turns out is fairly easy with a little bit of transpiling and module loaders and bundlers.
Arrow functions with =>
The purpose of arrow functions is twofold: to offer a simpler syntax to define functions, as well as to offer a more natural behavior for JavaScript's lexical scope and the this
execution context in functions.
An arrow function allows you to substitute the standard function(arguments) { function_body }
syntax into the shorter (arguments) => { function_body }
syntax. To get accustomed to arrow function syntax, it's easiest to think of the =>
symbol as the function
keyword shifted to the right of a function's arguments. For example, function(x) { return x * x }
is equivalent to the arrow function x => { return x * x }
, which is also equivalent to the arrow expression x => x * x
.
Listing 3-29 illustrates various examples of the fat arrow symbol used as a function expression, immediately-invoked function expression (IIFE), as well as a closure.
Listing 3-29. Arrow functions
// Arrow based expression let arrowEchoer = message => {console.log(message);return message}; arrowEchoer(arrowEchoer(arrowEchoer(arrowEchoer("Hello there from arrowEchoer()!")))); // Let's try an arrow based IIFE let primeNumbers = [2,3,5,7,11]; (() => { for (let k = 0; k < primeNumbers.length; k++) { console.log(primeNumbers[k]); } })(); // Arrow closure let countClosureArrow = (() => { let counter = 1; return () => {console.log(counter); counter += 1;} })(); countClosureArrow(); countClosureArrow(); countClosureArrow();
As you can see in listing 3-29, much of the motiviation behind arrow functions is due to scenarios where the verbose nature of function
statements complicates their interpretation. The arrowEchoer()
function in listing 3-29 is a simplified version of the echoer()
function in listing 3-6; the IIFE with a loop over the primeNumber
array in listing 3-29 is a simplified version of the IIFE with a loop in listing 3-19; and the countClosureArrow()
closure in listing 3-29 is a simplified version of the closure in listing 3-23
In this sense, arrow functions bring a welcomed relief to additional typing and visual overload.
But in addition, arrow functions also bring some normality to functions in JavaScript in terms of their execution context or this
reference. If you recall from the prior section on lexical scope and the this
execution context, all JavaScript objects and by extension function
declarations which are objects, have access to their own context through the this
keyword.
In certain circumstances, having a this
reference for every function()
can lead to extra workarounds you learned about in listing-3 20 (e.g. using that = this
or the call()
function to alter a function's default this
reference). Arrow functions on the other hand, do not have their own this
context, as illustrated in listing 3-30.
Listing 3-30. Arrow function's this
reference is from their outer scope
var tableTennis = {} tableTennis.counter = 0; tableTennis.playArrow = function() { // 'this' is the tableTennis object in this scope let swing = () => { // 'this' in arrow functions is the outer function object in this scope // can use 'this' to access outer scope this.counter++; } let ping = () => { // 'this' in arrow functions is the outer function object in this scope // can use 'this' to access outer scope console.log("Ping " + this.counter); } var pong = () => { // 'this' in arrow functions is the outer function object in this scope // can use 'this' to access outer scope console.log("Ping " + this.counter); } // Call inner functions in sequence swing(); ping(); pong(); } // Call tableTennis.playArrow() three times tableTennis.playArrow(); tableTennis.playArrow(); tableTennis.playArrow();
The example in listing 3-30 is an updated version of listing 3-22 that uses arrow functions. In this case, notice the inner swing()
, ping()
and pong()
functions of the playArrow
function use the this
reference directly and get access to the outer scope this
. This is possible because arrow functions don't have their own this
reference and instead gain automatic access to the outer scope this
reference due to lexical scoping, a behavior which make arrow functions quite popular in scenario like JavaScript callbacks which would otherwise require the workarounds presented in listing 3-22.
The void
keyword
Depending on their purpose, functions in JavaScript can return an explicit value or simply ignore this requirement. Both scenarios have been illustrated in previous examples, like the functions in listing 3-4 and listing 3-5 that don't return any value and the functions in listing 3-6 and listing 3-7 that use the return
keyword to return a value.
For functions that don't use a return
statement to return a value, JavaScript automatically returns an undefined
value, which is a JavaScript primitive data type.
This takes us to the similarly behaved void
keyword, which is an operator to evaluate expressions that always returns undefined
. In this sense, the void
operator behaves like a function that doesn't return a value, because it also evaluates an expression and returns undefined
by default. So what's the difference between using one or the other ? None at all, except you can still find some occurrences of the void
operator, although most are not widely used or no longer relevant.
One scenario where you can find the void
operator is in legacy HTML link elements that don't take users anywhere, such as <a href="javascript:void(0)">Button</a>
. Such links are created to keep the mouse hovering effects of an HTML link and simulate a button that does something else besides taking users to another url. So the href
attribute of an HTML link can be given a JavaScript handler -- javascript:
-- to execute any JavaScript function and the void
operator serves as practical mechanism to evaluate the expression inside it -- 0
does nothing, but it could also be a console statement or something else -- and return an undefined
value.
However, the HTML link attribute href="javascript:void(0)"
is now equivalent to the HTML link attribute href="javascript:undefined"
. Early JavaScript engines -- pre ES5 -- didn't recognize the undefined
value in the latter manner, therefore the void
operator had to be used to obtain an undefined
value. But with JavaScript engines and their global object now supporting undefined
, definitions with a statement like href="javascript:void(0)"
can be substituted with the simpler statement href="javascript:undefined"
.
A second scenario where you can find the void
operator is prefixed to functions that operate as Immediately-invoked function expressions (IIFE). Back in listing 3-18 and listing 3-19 you learned how an IIFE is used to deliver namespace support and block scoping using a syntax wrapped around parenthesis (e.g.(function testing(){ }());
).
Since the void
operator evaluates an expression and returns undefined
, it can be used in place of the outer parenthesis ( )
to evaluate the function and not return anything, delivering the same results. Listing 3-31 shows refactored versions of listing 3-18 and listing 3-19 that use IIFE with the void
operator.
Listing 3-31. IIFE with void
operator
// Contents of fantastic.js var fantasticNS = {}; void function(namespace) { namespace.render = function() { console.log("Hello from fantasticNS.render()!") }; }(fantasticNS); // Contents of wonderful.js var wonderfulNS = {}; void function() { this.render = function() { console.log("Hello from wonderfulNS.render()!") }; }.apply(wonderfulNS); // Contents of amazing.js var amazingNS = {}; void function() { var privateRender = function() { console.log("Hello from amazingNS.render()!") }; this.render = function() { privateRender() }; }.call(amazingNS); // Let's call the render() method for each of the different libraries fantasticNS.render(); wonderfulNS.render(); amazingNS.privateRenderer; // This does nothing because privateRenderer is local to IIFE block amazingNS.render(); // IIFE to avoid leaking loop var var primeNumbers = [2,3,5,7,11]; void function() { for (var k = 0; k < primeNumbers.length; k++) { console.log(primeNumbers[k]); } }(); console.log("The value of k is " + k); // k is not defined ReferenceError, k is out of scope by using IIFE
As you can see in listing 3-31, all the IIFE examples use the void
operator instead of parenthesis ()
and produce the same outcome. Since the appearance of JavaScript modules lessened the need for IIFE altogether, finding syntax like the one in listing 3-31 might not be all that common. If and when you need to declare an IIFE, I'll leave the judgment of which syntax to use to you.
Finally, a third scenario where you can find the void
operator is as a wrapper mechanism to ensure arrow functions always return an undefined
value. Let's assume you have a function called unreliableReturnValue()
, that's subject to returning various data types. If you want to ensure unreliableReturnValue()
runs its internal logic for its side-effects and not care about its return value so it always returns undefined
, you can use an arrow function in the following form: let safeUnreliable = () => void unreliableReturnValue()
. This way, if you call safeUnrealiable()
it triggers the execution of unreliableReturnValue()
and the void
operator ensures the result is always undefined
.
Function parameter default values
Briefly presented in listing 3-9 as part of the template literals topic, JavaScript functions also have the ability to define default values for their parameters. Function parameters in JavaScript (e.g. message
in function plainEchoer(message)
) prior to ES6 (ES2015), needed to be provided by the caller of a function or make use of conditional logic in the function itself (e.g. if (message !== undefined)...
) to operate with a default value. Now it's possible to use an equality symbol on function parameters to define them with a default value, as illustrated in listing 3-32.
Listing 3-32. Function parameter default values
// Function with default parameter value function plainEchoer(message="Hello World!") { console.log(message); return message; } plainEchoer(); plainEchoer("Hello there!"); // Arrow based expression with default parameter value let arrowEchoer = (message="Good day") => {console.log(message);return message}; arrowEchoer(); arrowEchoer("Good night");
In listing 3-32, you can see default function parameter values are used in case functions are called without parameter values, and for cases where functions are called with parameter values, these take precedence over function parameter default values.
Generators: Functions with *
and the yield
keyword
Generators[19] are a special construct used in programming languages to efficiently loop over a set of values. You know you're in the presence of a JavaScript generator when you see a function
statement followed by the *
symbol accompanied by return values making use of the yield
keyword.
Because generators are closely tied to the use of JavaScript for loops, they're explained in the generators and yield expressions section of said chapter. For the moment, just be aware that whenever you see a function*
statement and the use of the yield
keyword as part of a return statement, it means you're dealing with a generator.
The spread/rest ...
operator
Many programming languages support alternative approaches to achieve the same functionality while reducing typing and increasing readability, a mechanism often dubbed syntactic sugar on account of it being syntax that's sweeter for human consumption. JavaScript introduced the ...
operator to simplify cases where data elements required expansion. There are two scenarios where the ...
syntax is used, one is called spread operator syntax and the other rest parameter syntax.
The spread operator syntax is used for cases in which an Array reference needs to be expanded, avoiding the need to manually traverse its contents. The rest parameter syntax is used to define an Array parameter to handle an undetermined amount of parameters, avoiding the need to explicitly define parameters. Listing 3-33 illustrates the use of the spread/rest ...
operator
Listing 3-33. Spread/rest ...
operator
let vowels = ["a","e","i","o","u"]; let numbers = [1,2,3]; let randomLetters = ["c","z",...vowels,"b"]; let randomNumbers = [...numbers,7,9]; console.log(randomLetters); console.log(randomNumbers); function alphabet(...letters) { console.log(letters); } alphabet("a"); alphabet("a","b","c"); alphabet("m","n","o","p","q","r","s","t","u","v","w","x","y","z");
The first two examples in listing 3-33 illustrate how the spread operator syntax expands the contents of the vowels
and numbers
variables in the randomLetters
and randomNumbers
variables. The alphabet
function example in listing 3-33 makes use of the rest parameter syntax on the letters
parameter, demonstrating how it's possible to call the alphabet
function with different amounts of parameters and gain access to each one -- as array elements -- inside the body of the function.
The exponentiation operator **
Prior to ES7 (ES2016), in order to raise a number to the nth power you had to use the Math
data type (e.g. Math.pow(x, y)
). With the exponentiation operator, the syntax x**y
executes the same operation -- x
raised to the power y
-- using a simpler syntax.
Asynchronous functions with async
and await
You'll now when you encounter an asynchronous function when it's preceded by the async
keyword and the function uses the await
keyword inside its body. However, in order to understand the why behind asynchronous functions, it requires you comprehend how JavaScript dealt with asynchronous problems since the language's inception. For this reason, JavaScript asynchronous functions are described in the chapter dedicated to JavaScript asynchronous behavior.
Numeric separators with the underscore _
character
Numbers in JavaScript can be difficult to discern if they have too many digits to either the left or right of the decimal point. It's possible to use the underscore _
character to separate digits to allow the human eye to easily detect where units, hundreds or thousands start and end in a large number.
The underscore _
character has no influence over how an actual numeric value is stored and just works as a visual crutch. The underscore _
character can be used with both number
primitive data types and bigint
primitive data types.
Listing 3-34 illustrates a series of numeric value examples that use an underscore character _
as a numeric separator.
Listing 3-34. Numeric separator with the underscore _
character
// number primitive, with literal decimal number let a = 30_000_000_000; let b = 30.505_656_951; // number primitive, with literal exponential number let c = 30_000e1; let d = 300_000e-1; // number primitive, with literal binary number let e = 0B0000_0000_0000_0000_0000_0000_0001_1110; let f = 0b0000_0000_0000_0000_0000_0000_0001_1110; // number primitive, with literal octal number let g = 0O0_3_6; let h = 0o0_3_6; // number primitive, with literal hexadecimal number let i = 0X1E_AF_12; let j = 0x1E_BD_37; console.log("a is %s with value: %s", typeof a, a); console.log("b is %s with value: %s", typeof b, b); console.log("c is %s with value: %s", typeof c, c); console.log("d is %s with value: %s", typeof d, d); console.log("e is %s with value: %s", typeof e, e); console.log("f is %s with value: %s", typeof f, f); console.log("g is %s with value: %s", typeof g, g); console.log("h is %s with value: %s", typeof h, h); console.log("i is %s with value: %s", typeof i, i); console.log("j is %s with value: %s", typeof j, j); // bigint primitive, with literal number let k = 100_000_000n; let l = 9_007_199_254_740_993n; // Number.MAX_SAFE_INTEGER + 2 console.log("k is %s with value: %s", typeof k, k); console.log("l is %s with value: %s", typeof l, l);
Listing 3-34 first declares ten variables a
through j
with different number
primitives that use the underscore character _
as a numeric separator. Notice the _
separator is valid on all five number
primitive syntax variations -- decimal, exponential, binary, octal & hexadecimal -- and can also be used to separate units, hundreds or thousands. The console
statements that follow, confirm the underlying value for the number
primitives is unaffected by the _
separator.
The last part of listing 3-34 declares two bigint
primitives in the k
and l
variables, that also use the underscore character _
as a numeric separator. Notice how the console
statements that follow, also confirm the underlying value for the bigint
primitives is unaffected by the _
separator.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types ↑
https://html.spec.whatwg.org/multipage/scripting.html#the-script-element ↑
https://en.wikipedia.org/wiki/Above_the_fold#In_web_design ↑
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode ↑
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply ↑
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call ↑
https://en.wikipedia.org/wiki/Literal_(computer_programming)#Literals_of_objects ↑
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind ↑
https://www.ecma-international.org/ecma-262/6.0/#sec-let-and-const-declarations ↑
http://dojotoolkit.org/documentation/tutorials/1.10/modules/ ↑
https://en.wikipedia.org/wiki/Generator_(computer_programming) ↑