Declaring Variables in ES6
const
A constant is a variable that cannot be changed. Like other languages had done before it, JavaScript introduced constants with ES6.
Before constants, all we had were variables, and variables could be overwritten:
1 | var pizza = true |
We cannot reset the value of a constant variable, and it will generate a console error if we try to overwrite the value:
1 | const pizza = true |
let
JavaScript now has lexical variable scoping. In JavaScript, we create code blocks with curly braces ({}
). With functions, these curly braces block off the scope of variables. On the other hand, think about if/else
statements. If you’re coming from other languages, you might assume that these blocks would also block variable scope. This is not the case.
If a variable is created inside of an if/else
block, that variable is not scoped to the block:
1 | var topic = "JavaScript" |
The topic
variable inside the if
block resets the value of topic
.
With the let
keyword, we can scope a variable to any code block. Using let
protects the value of the global variable:
1 | var topic = "JavaScript" |
The value of topic
is not reset outside of the block.
Another area where curly braces don’t block off a variable’s scope is in for
loops:
1 | var div, |
In this loop, we create five div
s to appear within a container. Each div
is assigned an onclick
handler that creates an alert box to display the index. Declaring i
in the for
loop creates a global variable named i
, and then iterates it until its value reaches 5
. When you click on any of these boxes, the alert says that i
is equal to 5
for all div
s, because the current value for the global i
is 5
.
Declaring the loop counter i
with let
instead of var
does block off the scope of i
. Now clicking on any box will display the value for i
that was scoped to the loop iteration:
1 | var div, container = document.getElementById('container') |
Template Strings
Template strings provide us with an alternative to string concatenation. They also allow us to insert variables into a string.
Traditional string concatenation uses plus signs or commas to compose a string using variable values and strings:
1 | console.log(lastName + ", " + firstName + " " + middleName) |
With a template, we can create one string and insert the variable values by surrounding them with ${ }
:
1 | console.log(`${lastName}, ${firstName} ${middleName}`) |
Any JavaScript that returns a value can be added to a template string between the ${ }
in a template string.
Template strings honor whitespace, making it easier to draft up email templates, code examples, or anything else that contains whitespace. Now you can have a string that spans multiple lines without breaking your code.
Example: Template strings honor whitespace
1 | ` |
Previously, using an HTML string directly in our JavaScript code was not so easy to do because we’d need to run it together on one line. Now that the whitespace is recognized as text, you can insert formatted HTML that is easy to understand:
1 | document.body.innerHTML = ` |
Notice that we can include variables for the page title and article text as well.
Default Parameters
Languages including C++ and Python allow developers to declare default values for function arguments. Default parameters are included in the ES6 spec, so in the event that a value is not provided for the argument, the default value will be used.
For example, we can set up default strings:
1 | function logActivity(name="Shane McConkey", activity="skiing") { |
If no arguments are provided to the favoriteActivity
function, it will run correctly using the default values. Default arguments can be any type, not just strings:
1 | var defaultPerson = { |
Arrow Functions
Arrow functions are a useful new feature of ES6. With arrow functions, you can create functions without using the function
keyword. You also often do not have to use the return
keyword.
Example: As a traditional function
1 | var lordify = function(firstname) { |
With an arrow function, we can simplify the syntax tremendously.
Example: As an arrow function
1 | var lordify = firstname => `${firstname} of Canterbury` |
With the arrow, we now have an entire function declaration on one line. The function
keyword is removed. We also remove return
because the arrow points to what should be returned. Another benefit is that if the function only takes one argument, we can remove the parentheses around the arguments.
More than one argument should be surrounded by parentheses:
1 | // Old |
We can keep this as a one-line function because there is only one statement that needs to be returned.
More than one line needs to be surrounded with brackets:
1 | // Old |
These if/else
statements are surrounded with brackets but still benefit from the shorter syntax of the arrow function.
Arrow functions do not block this
. For example, this
becomes something else in the setTimeout
callback, not the tahoe
object:
1 | var tahoe = { |
This error is thrown because it’s trying to use the .join
method on what this
is. In this case, it’s the window object. Alternatively, we can use the arrow function syntax to protect the scope of this
:
1 | var tahoe = { |
This works correctly and we can .join
the resorts with a comma. Be careful, though, that you’re always keeping scope in mind. Arrow functions do not block off the scope of this
1 | var tahoe = { |
Changing the print
function to an arrow function means that this
is actually the window.
To verify, let’s change the console message to evaluate whether this is the window:
1 | var tahoe = { |
It evaluates as true
. To fix this, we can use a regular function:
1 | var tahoe = { |
Transpiling ES6
Not all web browsers support ES6, and even those that do don’t support everything. The only way to be sure that your ES6 code will work is to convert it to ES5 code before running it in the browser. This process is called transpiling. One of the most popular tools for transpiling is Babel.
In the past, the only way to use the latest JavaScript features was to wait weeks, months, or even years until browsers supported them. Now, transpiling has made it possible to use the latest features of JavaScript right away. The transpiling step makes JavaScript similar to other languages. Transpiling is not compiling: our code isn’t compiled to binary. Instead, it’s transpiled into syntax that can be interpreted by a wider range of browsers. Also, JavaScript now has source code, meaning that there will be some files that belong to your project that don’t run in the browser.
Below shows some ES6 code. We have an arrow function, already covered, mixed with some default arguments for x
and y
.
Example: ES6 code before Babel transpiling
1 | const add = (x=5, y=10) => console.log(x+y); |
After we run the transpiler on this code, here is what the output will look like:
1 | ; |
The transpiler added a “use strict” declaration to run in strict mode. The variables x
and y
are defaulted using the arguments
array, a technique you may be familiar with. The resulting JavaScript is more widely supported.
You can transpile JavaScript directly in the browser using the inline Babel transpiler. You just include the browser.js file, and any scripts with type="text/babel"
will be converted (even though Babel 6 is the current version of Babel, only the CDN for Babel 5 will work):
1 | <script |
TRANSPILING IN THE BROWSER
This approach means that the browser does the transpiling at runtime. This is not a good idea for production because it will slow your application down a lot. For now, the CDN link will allow us to discover and use ES6 features.
You may be thinking to yourself: “Great! When ES6 is supported by all browsers, we won’t have to use Babel anymore!” However, by the time this happens, we will want to use features of the next version of the spec. Unless a tectonic shift occurs, we’ll likely be using Babel in the foreseeable future.
ES6 Objects and Arrays
ES6 gives us new ways for working with objects and arrays and for scoping the variables within these datasets. These features include destructuring, object literal enhancement, and the spread operator.
Destructuring Assignment
The destructuring assignment allows you to locally scope fields within an object and to declare which values will be used.
Consider this sandwich
object. It has four keys, but we only want to use the values of two. We can scope bread
and meat
to be used locally:
1 | var sandwich = { |
The code pulls bread
and meat
out of the object and creates local variables for them. Also, the bread
and meat
variables can be changed:
1 | var {bread, meat} = sandwich |
We can also destructure incoming function arguments. Consider this function that would log a person’s name as a lord:
1 | var lordify = regularPerson => { |
Instead of using dot notation syntax to dig into objects, we can destructure the values that we need out of regularPerson
:
1 | var lordify = ({firstname}) => { |
Destructuring is also more declarative, meaning that our code is more descriptive about what we are trying to accomplish. By destructuring firstname
, we declare that we will only use the firstname
variable. We’ll cover more on declarative programming in the next chapter.
Values can also be destructured from arrays. Imagine that we wanted to assign the first value of an array to a variable name:
1 | var [firstResort] = ["Kirkwood", "Squaw", "Alpine"] |
We can also pass over unnecessary values with list matching using commas. List matching occurs when commas take the place of elements that should be skipped. With the same array, we can access the last value by replacing the first two values with commas:
1 | var [,,thirdResort] = ["Kirkwood", "Squaw", "Alpine"] |
Later in this section, we’ll take this example a step further by combining array destructuring and the spread operator.
Object Literal Enhancement
Object literal enhancement is the opposite of destructuring. It is the process of restructuring or putting back together. With object literal enhancement, we can grab variables from the global scope and turn them into an object:
1 | var name = "Tallac" |
name
and elevation
are now keys of the funHike
object.
We can also create object methods with object literal enhancement or restructuring:
1 | var name = "Tallac" |
Notice we use this
to access the object keys.
When defining object methods, it is no longer necessary to use the function
keyword.
Example: Old versus new: Object syntax
1 | // OLD |
Object literal enhancement allows us to pull global variables into objects and reduces typing by making the function
keyword unnecessary.
The Spread Operator
The spread operator is three dots (...
) that perform several different tasks. First, the spread operator allows us to combine the contents of arrays. For example, if we had two arrays, we could make a third array that combines the two arrays into one:
1 | var peaks = ["Tallac", "Ralston", "Rose"] |
All of the items from peaks
and canyons
are pushed into a new array called tahoe
.
Let’s take a look at how the spread operator can help us deal with a problem. Using the peaks
array from the previous sample, let’s imagine that we wanted to grab the last item from the array rather than the first. We could use the Array.reverse
method to reverse the array in combination with array destructuring:
1 | var peaks = ["Tallac", "Ralston", "Rose"] |
See what happened? The reverse
function has actually altered or mutated the array. In a world with the spread operator, we don’t have to mutate the original array; we can create a copy of the array and then reverse it:
1 | var peaks = ["Tallac", "Ralston", "Rose"] |
Since we used the spread operator to copy the array, the peaks
array is still intact and can be used later in its original form.
The spread operator can also be used to get some, or the rest, of the items in the array:
1 | var lakes = ["Donner", "Marlette", "Fallen Leaf", "Cascade"] |
We can also use the spread operator to collect function arguments as an array. Here, we build a function that takes in n number of arguments using the spread operator, and then uses those arguments to print some console messages:
1 | function directions(...args) { |
The directions
function takes in the arguments using the spread operator. The first argument is assigned to the start
variable. The last argument is assigned to a finish
variable using Array.reverse
. We then use the length of the arguments
array to display how many towns we’re going through. The number of stops is the length of the arguments
array minus the finish
stop. This provides incredible flexibility because we could use the directions
function to handle any number of stops.
The spread operator can also be used for objects. Using the spread operator with objects is similar to using it with arrays. In this example, we’ll use it the same way we combined two arrays into a third array, but instead of arrays, we’ll use objects:
1 | var morning = { |
Promises
Promises give us a way to make sense out of asynchronous behavior. When making an asynchronous request, one of two things can happen: everything goes as we hope or there’s an error. There may be several different types of successful or unsuccessful requests. For example, we could try several ways to obtain the data to reach success. We could also receive multiple types of errors. Promises give us a way to simplify back to a simple pass or fail.
Let’s create an asynchronous promise for loading data from the randomuser.me API. This API has information like email address, name, phone number, location, and so on for fake members and is great to use as dummy data.
The getFakeMembers
function returns a new promise. The promise makes a request to the API. If the promise is successful, the data will load. If the promise is unsuccessful, an error will occur:
1 | const getFakeMembers = count => new Promise((resolves, rejects) => { |
With that, the promise has been created, but it hasn’t been used yet. We can use the promise by calling the getFakeMembers
function and passing in the number of members that should be loaded. The then
function can be chained on to do something once the promise has been fulfilled. This is called composition. We’ll also use an additional callback that handles errors:
1 | getFakeMembers(5).then( |
Promises make dealing with asynchronous requests easier, which is good, because we have to deal with a lot of asynchronous data in JavaScript. You’ll also see promises used heavily in Node.js, so a solid understanding of promises is essential for the modern JavaScript engineer.
Classes
Previously in JavaScript, there were no official classes. Types were defined by functions. We had to create a function and then define methods on the function object using the prototype:
1 | function Vacation(destination, length) { |
If you were used to classical object orientation, this probably made you mad.
ES6 introduces class declaration, but JavaScript still works the same way. Functions are objects, and inheritance is handled through the prototype, but this syntax makes more sense if you come from classical object orientation:
1 | class Vacation { |
CAPITALIZATION CONVENTIONS
The rule of thumb with capitalization is that all types should be capitalized. Due to that, we will capitalize all class names.
Once you’ve created the class, you can create a new instance of the class using the new
keyword. Then you can call the custom method on the class:
1 | const trip = new Vacation("Santiago, Chile", 7); |
Now that a class object has been created, you can use it as many times as you’d like to create new vacation instances. Classes can also be extended. When a class is extended, the subclass inherits the properties and methods of the superclass. These properties and methods can be manipulated from here, but as a default, all will be inherited.
You can use Vacation
as an abstract class to create different types of vacations. For instance, an Expedition
can extend the Vacation
class to include gear:
1 | class Expedition extends Vacation { |
That’s simple inheritance: the subclass inherits the properties of the superclass. By calling the printDetails
method of Vacation
, we can append some new content onto what is printed in the printDetails
method of Expedition
. Creating a new instance works the exact same way—create a variable and use the new
keyword:
1 | const trip = new Expedition("Mt. Whitney", 3, |
CLASSES AND PROTOTYPAL INHERITANCE
Using a class still means that you are using JavaScript’s prototypal inheritance. Log Vacation.prototype
, and you’ll notice the constructor and printDetails
methods on the prototype.
We will use classes a bit in this book, but we’re focusing on the functional paradigm. Classes have other features, like getters, setters, and static methods, but this book favors functional techniques over object-oriented techniques. The reason we’re introducing these is because we’ll use them later when creating React components.
ES6 Modules
A JavaScript module is a piece of reusable code that can easily be incorporated into other JavaScript files. Until recently, the only way to work with modular JavaScript was to incorporate a library that could handle importing and exporting modules. Now, with ES6, JavaScript itself supports modules.
JavaScript modules are stored in separate files, one file per module. There are two options when creating and exporting a module: you can export multiple JavaScript objects from a single module, or one JavaScript object per module.
In text-helpers.js, the module and two functions are exported.
Example: ./text-helpers.js
1 | export const print(message) => log(message, new Date()) |
export
can be used to export any JavaScript type that will be consumed in another module. In this example the print
function and log
function are being exported. Any other variables declared in text-helpers.js will be local to that module.
Sometimes you may want to export only one variable from a module. In these cases you can use export default
.
Example: ./mt-freel.js
1 | const freel = new Expedition("Mt. Freel", 2, ["water", "snack"]) |
export default
can be used in place of export
when you wish to export only one type. Again, both export
and export default
can be used on any JavaScript type: primitives, objects, arrays, and functions.
Modules can be consumed in other JavaScript files using the import
statement. Modules with multiple exports can take advantage of object destructuring. Modules that use export default
are imported into a single variable:
1 | import { print, log } from './text-helpers' |
You can scope module variables locally under different variable names:
1 | import { print as p, log as l } from './text-helpers' |
You can also import everything into a single variable using *
:
1 | import * as fns from './text-helpers |
ES6 modules are not yet fully supported by all browsers. Babel does support ES6 modules, so we will be using them throughout this book.
CommonJS
CommonJS is the module pattern that is supported by all versions of Node.js. You can still use these modules with Babel and webpack. With CommonJS, JavaScript objects are exported using module.exports
.
Example: ./txt-helpers.js
1 | const print(message) => log(message, new Date()) |
CommonJS does not support an import
statement. Instead, modules are imported with the require
function:
1 | const { log, print } = require('./txt-helpers') |