Modern JavaScript, and how to enforce quality through language features
As discussed previously, one of the most basic (and therefore powerful) tools we have as developers are the features of the languages we use.
Just as when we write or speak in a natural language Opens in a new window (as opposed to a computer language), we can make ourselves better understood by being precise.
Some of the recently added features in JavaScript are a good example of this. Today we'll look at two newer features that let us enhance the quality of our applications by being more descriptive and better organized.
Before we begin, we have to have the standard talk about using newer features.
Ideally, your code would work in every browser, through every version, and on every device and operating system.
Here's a list of browsers with >= 0.01% market share in Canada, as of April 2021:
Date | Chrome | Safari | Firefox | Edge | Samsung Internet | Edge Legacy | IE | Opera | Android | Mozilla | 360 Safe Browser | UC Browser | Sony PS4 | QQ Browser | Unknown | Sogou Explorer | Yandex Browser | Maxthon | Chromium | BlackBerry | Puffin | Pale Moon | Coc Coc | Other |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2021‑04 | 54.09 | 28.32 | 5.17 | 5.57 | 3.26 | 0.79 | 0.83 | 0.76 | 0.37 | 0.23 | 0.18 | 0.13 | 0.06 | 0.04 | 0.03 | 0.02 | 0.03 | 0.02 | 0.01 | 0.02 | 0.01 | 0.01 | 0.01 | 0.05 |
Data via Statcounter Opens in a new window |
Date | Chrome | Safari | Firefox | Edge | Samsung | Edge Legacy | IE |
---|---|---|---|---|---|---|---|
2021‑04 | 54.09 | 28.32 | 5.17 | 5.57 | 3.26 | 0.79 | 0.83 |
Data via Statcounter Opens in a new window. See notes for full list. |
Some of these browsers I've never heard of, and some Opens in a new window browsers Opens in a new window I personally use aren't on the list!
The good news: the majority of these browsers use the same "engine" - Chromium - meaning that feature support is at least somewhat consistent. It would be a lot tougher if they were all built with different underlying technologies.
We're still left with the fact, however, that 100% bulletproof cross-browser support isn't possible. This means that supported browsers is an important requirement to document.
That said, which browsers can you expect to be required in most situations, and how do you support them?
Your first stop for determining feature support across different browsers should be caniuse.com Opens in a new window.
caniuse provides detailed information about support for HTML, CSS, and JavaScript features (along with other front-end technologies). Have a look at feature support for ES6 broadly Opens in a new window, but know that you can search for individual features.
Because that's the thing about newer features on the front-end: they don't get enabled in broad categories like "CSS3", "HTML5" or "ES6" anymore - they get rolled out one feature at a time.
In the bad old days, we used to use a technique called "browser sniffing" Opens in a new window, or "device sniffing". In this technique, you use various hacks to determine the user's hardware and/or software, and serve different code features accordingly. It didn't work very well, and wasn't future-proof at all.
Nowadays we use two techniques - transcompilation (turning our new code into old code), and "cutting the mustard".
In transcompilation, we run our code through a "transcompiler". The most popular transcompiler is Babel Opens in a new window. Babel takes your fancy new code, and turns it into code that will run in older browsers by writing out our newer shorthands into older, longer code, and by adding "polyfills" (extra code that will create newer features in the older version of the language) where needed.
"Cutting the mustard" means checking to see if a newer feature is available by invoking it. If it's not available, a fallback is used.
Here's a very simple version of a "mustard cut":
<html lang="en" class="no-js">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
document.documentElement.classList.remove('no-js');
document.documentElement.classList.add('js');
</script>
<style>
.content-no-js {
display: none;
}
.no-js .content-no-js {
display: block;
}
</style>
</head>
<body>
<div class="content-no-js">
Looks like you've
turned off JavaScript
¯\_(ツ)_/¯
</div>
</body>
</html>
Transcompilation lets us write modern JavaScript while supporting older browsers, and mustard cuts let us use progressive enhancement to serve newer features where available, while still providing basic features everywhere. We can have a good idea of what features are widely supported by looking them up on caniuse.com.
Now that we know how to responsibly use newer features, let's look at some modern JavaScript!
Like variables? Me too!
var doingThis = "terrific";
One thing that can go wrong with variables, however, is changing their value when you didn't mean to.
doingThis = "not good if you didn't want to";
In the name 'variables' is the idea that sometimes you'll want to change values. But sometimes we do it accidentally - overriding another developer's code, perhaps, or just forgetting we already used a variable name when we're 500 lines deep into our code.
If only there were some way we could say to our code, "I intend to change this value", or "I don't want this value to be changed".
That's where let
and const
come in!
Both let
and const
work just like var
, except let
will 'let' you change the value, and const
will not let you change the value (because it is "constant").
And... that's about it, really!
A few things to note:
You can update let
's value, but you can't re-declare it:
let myVar = 5;
myVar = 10; // Totally ok.
let myVar = 15; // Not ok.
Unlike var
, let
& const
are block-scoped.
// `var` doesn't care about your blocks
var myVar = "Oh hello there";
var boolean = true;
if (boolean === true) {
var myVar = "Goodbye";
console.log(myVar);
// Logs "Goodbye"
}
console.log(myVar);
// Logs "Goodbye"
This means we can define a variable with a separate name in different blocks without the inner block affecting the outer block.
let myVar = "Oh hello there";
let boolean = true;
if (boolean === true) {
let myVar = "Goodbye";
// Logs "Goodbye"
console.log(myVar);
}
console.log(myVar);
// Logs "Oh hello there"
However if we redefine the variable in the inner block, it will affect the outer block.
let myVar = "Oh hello there";
let boolean = true;
if (boolean === true) {
myVar = "Goodbye";
// Logs "Goodbye"
console.log(myVar);
}
console.log(myVar);
// Logs "Goodbye"
Const behaves the same as in the above examples with regard to defining a variable in a separate block:
const myVar = "Oh hello there";
const boolean = true;
if (boolean === true) {
const myVar = "Goodbye";
// Logs "Goodbye"
console.log(myVar);
}
console.log(myVar);
// Logs "Oh hello there"
As you'd expect, it doesn't allow redefinition at all:
const myVar = "Oh hello there";
const boolean = true;
if (boolean === true) {
myVar = "Goodbye"; // Syntax Error
}
myVar = "Something else"; // Syntax Error
One thing to note with const
is that you have to give it a value when you define it.
This makes sense - since you can't redefine it, how would it ever have a value, and what's the point of a variable without a value?
const useless; // Syntax Error
The other thing with const
is that you can change the properties of constant values. What do I mean by that? I mean that you can add or remove values from an array or object that was declared as a constant, but you cannot change the array or the object itself.
const myArray = [];
myArray.push('7');
console.log(myArray);
// Logs ["7"]
myArray = ["8"];
// Syntax Error
const myObj = { myKey: "my value" };
myObj.myKey = "a different value";
console.log(myObj);
// Logs { "myKey": "a different value" }
myObj = { myKey: "my value" };
// Syntax error
In the last couple weeks, I mentioned JavaScript's status as a "loose", or "permissive" language. If you want to tighten things up (which you do, for both quality and security reasons), you can write JavaScript in "strict mode".
Entering strict mode is really easy. All you have to do is put this at the top of your JavaScript file:
'use strict';
Strict mode was introduced in the "ES5" version of JavaScript (way back in 2009), and is widely supported in browsers all the way back to, and including, IE10.
Feel free to read more about what strict mode disallows Opens in a new window, but, to be frank, it's all stuff I would have to teach you how to do in order to teach you how not to do it, and I don't know if that's the best use of our time.
Suffice it to say that if you're doing something, and adding 'use strict';
to your code prevents you from doing it, you shouldn't have been doing that thing.
You should enable strict mode for your JavaScript. Unless you're using modules, because modules have strict mode enabled by default!
JavaScript modules are JavaScript files, containing normal JavaScript.
They take advantage of two features introduced in ES6 - import
and export
.
This lets you choose which JavaScript variables end up where.
Modules are also a great way to break up our code into smaller, better-organized pieces, which makes code reuse much simpler.
Say you have a page that loads two JavaScript files - /script.js
, and /other-script.js
.
On line 700 of /script.js
, you've got this code:
const myNumber = 700;
On line 17 of /other-script.js
, you've got this code:
const myNumber = 701;
On its own, /other-script.js
is valid JavaScript, but when both files are loaded on the page, you get
Uncaught SyntaxError:
Identifier 'myNumber' has already been declared
This is what's called namespace pollution. Namespace pollution is when there's too much going on in the global scope, and everybody's variables and functions are all hanging out together. It gets messy.
Modules, specifically the import
and export
statements, let us be explicit about what we're using, and where we're using it. No more namespace pollution!
JavaScript files can get big. Like, really big. The average amount of JavaScript transferred Opens in a new window for every web page is roughly 2-3 times the size of the average Opens in a new window novel Opens in a new window.
We need a way to manage how we work on all that code!
That's where modules come in.
Now, in the previous example, if we loaded our JavaScript files in the traditional way, we produced an error.
const myNumber = 700;
/other-script.jsconst myNumber = 701;
/index.html<script src="script.js"></script>
<script src="other-script.js"></script>
<script>
console.log("My number is " + myNumber);
</script>
The ConsoleUncaught SyntaxError:
Identifier 'myNumber' has already been declared
My number is 700
Simply by adding the attribute value type="module"
to the script tags in our HTML, we get rid of that error - and replace it with a new one.
const myNumber = 700;
/other-script.jsconst myNumber = 701;
/index.html<script src="script.js" type="module"></script>
<script src="other-script.js" type="module"></script>
<script>
console.log("My number is " + myNumber);
</script>
The ConsoleUncaught ReferenceError: myNumber is not defined
This is because the variables have been taken out of the global scope - meaning they're no longer fighting to define the variable myNumber
.
Of course, we want to be able to use variables in different places, like in the above example.
To have this example work as intended, we need to make use of the import
and export
statements.
const myNumber = 700;
/other-script.jsconst myNumber = 701;
export { myNumber };
/index.html<script src="script.js" type="module"></script>
<script src="other-script.js" type="module"></script>
<script type="module">
import { myNumber } from './other-script.js';
console.log("My number is " + myNumber);
</script>
The ConsoleMy number is 701
In this example, we can see that export
says, "make this thing available", and import
says, "I want this thing".
Let's rearrange this in a few ways and see what happens.
Let's try importing a variable that the imported scripts has declared, but hasn't exported.
const myNumber = 701;
const myOtherNumber = 702;
export { myNumber };
/index.html<script src="other-script.js" type="module"></script>
<script type="module">
import { myOtherNumber } from './other-script.js';
console.log("My number is " + myOtherNumber);
</script>
The ConsoleUncaught SyntaxError:
The requested module './other-script.js' does not
provide an export named 'myOtherNumber'
Now let's using a variable that is exported, but not imported.
const myNumber = 701;
const myOtherNumber = 702;
export { myOtherNumber };
/index.html<script src="other-script.js" type="module"></script>
<script type="module">
import { myNumber } from './other-script.js';
console.log("My number is " + myOtherNumber);
</script>
The ConsoleUncaught SyntaxError:
The requested module './other-script.js' does not
provide an export named 'myOtherNumber'
We get the same error regardless of whether the variable is exported, but not imported, or imported, but not exported. Just a heads-up for if you're debugging based on this error.
Can you export and/or import more than one thing? Absolutely.
const myNumber = 701;
const myOtherNumber = 702;
export { myNumber, myOtherNumber };
/index.html<script src="other-script.js" type="module"></script>
<script type="module">
import { myNumber, myOtherNumber }
from './other-script.js';
console.log("My number is " + myNumber);
console.log("My other number is " + myOtherNumber);
</script>
The ConsoleMy number is 701
My other number is 702
I see that you have to export everything you want to import, but do you have to import everything that's being exported? Nope.
const myNumber = 701;
const myOtherNumber = 702;
export { myNumber, myOtherNumber };
/index.html<script src="other-script.js" type="module"></script>
<script type="module">
import { myOtherNumber } from './other-script.js';
console.log("My other number is " + myOtherNumber);
</script>
The ConsoleMy other number is 702
So, the big idea here is that we're not importing the whole file, just stuff that gets exported. Everything else gets stays within the module file.
A few things to note about modules:
Modules are more secure than regular JS files. A side-effect of this is that you cannot serve them locally. When developing locally with modules, you must use an HTTP server, like we did with some of our ad hoc testing tools.
Modules are in strict mode by default. You don't need to declare "use strict";
at the top of your modules - it's already happening.
Also, you can note that JavaScript modules are deferred by default.
Back in the day, we used to speed up our pages by putting our script tags at the bottom of our html content:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- All the content first -->
<script
type="text/javascript"
src="/scripts/script.js"></script>
</body>
</html>
Nowadays, of course, we can accomplish the same thing with the defer
attribute.
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width">
<title>Document</title>
<script src="/scripts/script.js" defer></script>
</head>
<body>
<!-- All the content -->
</body>
</html>
If we load our script as a module, however, that's taken care of.
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width">
<title>Document</title>
<script
src="/scripts/script.js"
type="module"></script>
</head>
<body>
<!-- All the content -->
</body>
</html>
So modules are safer, higher-quality, better organized and easier to develop? What's not to love?
Now, does all this mean that we should be serving a whole bunch of JavaScript as separate HTTP request in our production JavaScript? Heck no.
Modules are great for use when writing Node (which we'll cover in two weeks), and for development - there's no sense in having two developers working on separate features but pushing to the same file in our version control system.
Once you're in production and running code on the end-user's browser, however, it's a different story. There's been a lot of efforts made over the last few years to reduce the impact of multiple HTTP requests on page performance, but not enough yet that we can recommend serving many small files over one or two big ones.
What we do instead is use modules in development, and then "bundle" the files using our build process - turning our many development files into a single file that gets served to the end-user.
There's been a lot of strong contenders for "best bundler" in the last year or two, but webpack Opens in a new window is still the most-used tool. Keep an eye out though - esbuild Opens in a new window, Parcel Opens in a new window and rollup Opens in a new window all have the potential to become the front-runner in the next few years.
There's a few more features in modern JavaScript that, as we continue this journey, you'll want to be able to recognize.
These are outside of the scope of today's lesson - this is a Security & QA course, after all, not Intro to Intermediate JavaScript, despite how we've been spending our time lately. However, as you continue to learn, you'll see these pop up, and I want you to have at least a vague idea of what you're looking at.
Some of these are fairly recent developments, so make sure you check their browser support Opens in a new window before using them in a web application.
Arrows are just a shorthand syntax for functions.
const oldMultiply = function(x,y) {
return x * y;
}
const arrowMultiply = (x,y) => x * y;
console.log("The old way", oldMultiply(2,4));
console.log("The new way", arrowMultiply(2,4));
"The old way" 8
"The new way" 8
const myArray = [5,3,1];
myArray.forEach(function(element) {
console.log("The old way", element);
});
myArray.forEach(element => {
console.log("The new way", element);
});
"The old way" 5
"The old way" 3
"The old way" 1
"The new way" 5
"The new way" 3
"The new way" 1
Template literals are a new way of using variables, expressions and line breaks in strings. They use backticks (`
) instead of quotes around the string, and curly braces preceded by a dollar sign (${}
) for any variables or statements that need to be evaluated.
const str = "strings";
console.log("This is the old way to write " + str
+ "\nand include values like " + 2 * 10);
console.log(`This is the new way to write ${str}
and include values like ${2 * 10}`);
"This is the old way to write strings and include values like 20" "This is the new way to write strings and include values like 20"
Fetch is the new syntax for AJAX - getting resources in JavaScript without reloading the page.
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => console.log(json))
The Console{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
Okay, there could be an entire course on asynchronous JavaScript. Or maybe a month of classes. It's complicated is what I'm trying to say.
Basically, sometimes things in JavaScript take time, like waiting for an API to respond, or, in Node, waiting for the computer to finish reading a file.
Asynchronous JavaScript gives you the ability to say, "I want to wait for something before doing other things".
First we got promises Opens in a new window, but the syntax for handling them was pretty messy and awkward, so we got this new syntax for handling promises called async/await.
You declare that a function will be asynchronous with the keyword async
. Then, with anything inside that function that is going to take some time (anything that is a Promise
), you use the statement await
. Anything in the function that comes after the await
will wait on the Promise to resolve (i.e. finish) before executing.
console.clear();
async function myFetch() {
let response = await fetch('https://jsonplaceholder.typicode.com/photos/1')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
myFetch().then((data) => {
console.log(data.thumbnailUrl);
const image = document.createElement('img');
image.src = data.thumbnailUrl;
document.body.appendChild(image);
}).catch((e) =>
console.log("ok so there's an error", e)
);
let
is like var
, except block-scoped and explicitly re-definable.const
is like var
, except block-scoped and explicitly not re-definable.