Benefits of ES2015
Although we briefly touched upon ES2015 (ECMAScript 2015) in the previous chapter, it's possible that you may not be very familiar with it. Most browsers (especially mobile) do have full support for ES2015, if any at all. This is unfortunate, because there are tremendous improvements in syntax and semantics that make it easier to create complex mobile applications.
Thankfully, as you saw in the previous chapter, there are tools that port this functionality back to ES5, which is very well supported in modern browsers. These tools convert the ES2015 syntax and semantics into their ES5 equivalent (where possible). Where a perfect equivalence is not possible, the conversion will get as close as possible, which will still bring us great benefit.
We can't go over every new change in ES2015 in this book; but we do need to go over some common examples, especially if you aren't familiar with the syntax.
Note
If you are developing for older devices, you will want to pay special attention to the performance characteristics of your application if you intend on using ES2015 code. ES2015 code is not necessarily faster than ES5, especially when it is transpiled. Unfortunately, even the native implementations of ES2015 aren't always faster, so you must balance the benefits of developer productivity and program readability with that of the actual performance of your app.
Block scope
Correctly understanding variable scope is one very common developer pitfall developers encountered in ES5. Instead of being block-scoped (which is typical of most languages that have the block concept), the variables were function-scoped. Variable definitions were hoisted to the top of the function definition, which meant that it was easy to use variables before they were actually assigned a value and it was also easy to redefine variables (often typical in for
loops). Because this hoisting wasn't obvious from the source code, it was very easy to end up with nasty surprises.
Block-scoped variables, on the other hand, allow us to indicate that a variable is valid for a very specific portion of a function. We can reuse the name of the variable when we like (benefiting for
loops) without worrying about unwanted side effects or surprises. Let's look at the difference in how these are defined and assigned:
Note
The snippets in this section are located at snippets/02/ex1-block-scope/a
and snippets/02/ex1-block-scope/b
in the code package of this book. Using the interactive snippet playground, select 2: ES2015 and Browserify in the first dropdown and examples 1a and 1b in the second.
What's not terribly obvious in the previous examples is the difference in block scope. Consider this example in ES5:
// Snippet Example 1a // Snippet Example 1a var i = 10, // [1] x = 0; console.log(i); // 10 for (var i = 0, l = 5; i < l; i++) { // [2] x += i; } console.log(x); // 10 (0+1+2+3+4) console.log(i); // 5 [3] console.log(l); // 5
Notice that the value of i
defined in [1]
is overwritten by the for
loop in [2]
(as you can see in [3]
). Based upon the way the code is written, this is a surprise, especially to anyone who doesn't understand that the variable is function-scoped. Now, let's see the code in ES2015:
// Snippet Example 1b let i = 10, x = 0; console.log(i); // 10 for (let i = 0, l = 5; i < l; i++) { x += i; } console.log(x); // 10 (0+1+2+3+4) console.log(i); // 10 console.log(l); // Error: Can't find variable: l
Note that i
is no longer overwritten—a new variable was created for the duration of the loop. Furthermore, note that l
is not available at all; it fell out of scope when the loop terminated.
Preventing multiple definitions of variables is another important feature of let
and const
. ES5 didn't complain when a variable was defined more than once, which is what lead to the nasty surprise in the preceding example. ES2015, on the other hand, will complain if you attempt to redefine a variable within the same scope. For example:
let i = 10; let i = 20; // Error: Duplicate definition "i"
Tip
It's vital that you recognize that var
has not gone away; it's perfectly valid to define a variable using var
. If you do, however, you need to be aware that such variables operate using function-scope and do not inherit any ES2015 behavior.
Arrow functions
JavaScript has always relied upon the function
keyword heavily and this has often resulted in some hard-to-read code with a lot of boilerplate. Consider this ES5 snippet:
// Snippet Example 2a [1, 2, 3].map(function(val) { return val * 2; }).forEach(function(val) { console.log(val); }); // console displays: // 2 // 4 // 6
Note
The snippets in this section are located at snippets/02/ex2-fat-arrow-functions/a
and snippets/02/ex2-fat-arrow-functions/e
in the code package of this book. Using the interactive snippet playground, select 2: ES2015 and Browserify in the first dropdown and examples 2a-2e in the second dropdown.
Although this snippet is pretty simple to follow, it's a little annoying to type function
and return
all the time. With ES2015, we can use the following arrow syntax instead:
// Snippet Example 2b [1, 2, 3].map(val => val * 2) .forEach(val => console.log(val)); // console displays the same output as before
The form of these arrow functions differ based on the required number of parameters. Here are some examples:
// no parameters; note the empty parentheses [1, 2, 3].map(() => 2); // [2, 2, 2] // one parameter; no parentheses required [1, 2, 3].map(val => val * 2); // [2, 4, 6] // multiple parameters; parentheses required [1, 2, 3].map((val, idx) => val * idx); // [0, 2, 6]
Notice the lack of any return
keyword; the result of the expression after the arrow is assumed to be returned. Assuming the implicit return
is valid only for a single expression; if you need multiple statements, you must use braces and supply return
, as follows:
[1, 2, 3].map((val, idx) => { console.log(val); return val * idx; });
Arrow functions also have the concept of a lexical this
value. What exactly this
refers to at any point in time in JavaScript has always been a bit difficult to wrap one's mind around, especially while learning the language. It still often trips up even experienced developers. The problem arises when you do something like the following:
// Snippet Example 2c var o = { a: [1, 2, 3], b: 5, doMul: function() { return this.a.map(function(val) { return val * this.b; }); } } console.log(o.doMul());
Although this seems like a perfectly innocuous segment of code (we would expect [5, 10, 15]
on the console), it will actually throw an error: undefined is not an object (evaluating this.b)
. The this
reference inside doMul
refers to the object o
, but when this
is used inside the mapping function, it doesn't refer to o
. We can get around this by changing the code slightly, as follows:
// Snippet Example 2d var o = { ..., doMul: function() { return this.a.map(function(val) { return val * this.b; }, this); // map accepts a this parameter to apply to the callback } }
For any methods that don't accept a this
parameter, we could also use bind
and its cousins. But what if we didn't have to worry about it at all? In ES2015, we can write it as follows:
// Snippet Example 2e let o = { a: [1, 2, 3], b: 5, doMul() { return this.a.map(val => val * this.b); } }
Note
ES2015 doesn't eliminate the various vagaries with this
, so you still need to be on the lookout for any misuse of this
in your own code. But the lexical this
value definitely helps in easing this particular pain point.
Because arrow functions have their own lexical context, however, you can't use them everywhere you might use regular functions. If the function needs to be supplied with many different contexts using bind
(or similar), you'll want to use a regular function instead.
Simpler object definitions
Object definitions in JavaScript are fairly verbose. For example, consider the following code snippet:
// Snippet Example 3a function makePoint(x, y) { return { x: x, y: y }; } console.log(makePoint(5, 10)); // {x: 5, y: 10}
Note
The snippets in this section are located at snippets/02/ex3-object-definitions/a
and snippets/02/ex3-object-definitions/b
in the code package of this book. Using the interactive snippet playground, select 2: ES2015 and Browserify in the first dropdown and examples 3a and 3b in the second dropdown.
In the previous example, it seems a bit redundant to specify x
and y
twice, doesn't it? Although, in this example, it's not particularly onerous, in ES2015, we can write the following, which is much more concise:
// Snippet Example 3b function makePoint (x, y) { return {x, y}; } console.log(makePoint(5,10)); // {x: 5, y: 10}
In ES2015, the key name is optional. If it isn't provided, ES2015 assumes the key name should match that of the variable. In the preceding example, the resulting object will have two keys, namely x
and y
, and they will have the values passed to the routine.
Functions can also be specified using this shorthand, as you saw in the previous section. Instead of typing myFunction: function() {…}
, ES2015 allows you to specify myFu
nction() {…}
.
Default arguments
It is very common to write functions that have optional parameters. When these parameters aren't supplied, a default value is supplied instead. Unfortunately, the way this is often implemented is fraught with issues and potential surprises, and has led to many bugs. In ES5, default parameters are often handled as follows:
// Snippet Example 4a function mul(a, b, log) { var ans = a * b; if (log === true) { console.log(a, b, ans); } } mul(2, 4); // doesn't log anything mul(4, 8, true); // logs 4 8 32
Note
The snippets in this section are located at snippets/02/ex4-default-arguments/a
and snippets/02/ex4-default-arguments/b
in the code package of this book. Using the interactive snippet playground, select 2: ES2015 and Browserify in the first dropdown and examples 4a and 4b in the second dropdown.
While this technically works, it is far from ideal in because it isn't obvious from a quick read over the code that log
really is optional from a quick read over the code. Furthermore, if the variable isn't checked correctly, it's easy to end up in surprising situations (for example, if (log)
is not the same thing as if (log===true)
).
Note
It's also quite common to see the following pattern:
function repeat (a, times) { for (var I = 0; i < (times || 1) /*[1]*/; i++) { console.log(a); } }
Unfortunately this will lead to incorrect behavior if we were to call this method with times
set to zero. Because zero is falsy, the expression in [1] will actually evaluate to one, not zero.
It would be nice if we could avoid this mess entirely. ES2015 lets us write the earlier code in the following way:
// Snippet Example 4b function mul(a, b, log = false) { var ans = a * b; if (log === true) { // [1] console.log(a, b, ans); } }
This does nothing regarding JavaScript's concept of truthiness (so, we should still include the check in [1]
). However, it does make it obvious to the reader that log
is optional, and if it isn't supplied, what value it will receive.
Variable arguments
Another common pattern in JavaScript is to write methods that accept a variable number of arguments. A common example is the console.log
method—it accepts several values and logs each to the console. We can follow its example to write a simple sum
method. Let's see this first in ES5:
// Snippet Example 5a function sum() { var args = [].slice.call(arguments); return args.reduce( function(prev, cur) { return prev + cur; }, 0); } console.log(sum(1, 2, 3, 4, 5)); // 15
Note
The snippets in this section are located at snippets/02/ex5-variable-arguments/a
...d
in the code package of this book. Using the interactive snippet playground, select 2: ES2015 and Browserify in the first dropdown and examples 5a…5d in the second dropdown
Of course, this isn't ideal—it's not immediately obvious that this method can accept any number of parameters. ES2015 lets us indicate this explicitly by writing the code as follows:
// Snippet Example 5b function sum(...items) { return items.reduce((prev, cur) => (prev + cur), 0); }
There's not a significant difference so let's take a look at a slightly more complex example, again in ES5 first:
// Snippet Example 5c function interpolate(str) { var args = [].slice.call(arguments, 1 /*[1]*/); return args.reduce(function(prev, cur, idx) { return prev.replace(new RegExp(":" + (idx + 1), "g"), cur); }, str); } console.log(interpolate("My name is :1 and I say :2", "Bob", "Hello")); // My name is Bob and I say Hello
As before, this function signature doesn't indicate that interpolate
accepts multiple parameters. Furthermore, we had to modify the code that parses all the additional parameters slightly—notice we added 1
while calling slice
at [1]
. This is because we needed to avoid including the first named parameter when performing the interpolation.
In ES2015, we can write the following instead:
// Snippet Example 5d function interpolate(str, ...items) { return items.reduce((prev, cur, idx) => prev.replace( new RegExp(":" + (idx + 1), "g"), cur), str); }
The …
operator can be used in another form in ES2015 by calling interpolate
this way:
console.log(interpolate("My name is :1 and I say :2", ...["Bob", "Hello"]));
Granted, it looks a little silly when we hard-code the array. But if we need to write this in ES5, we'd have to use interpolate.apply (null,["...","Bob","Hello"])
to achieve the same functionality. This proves incredibly useful when an array is passed to you; you can easily call a function that accepts a variable number of parameters
Destructuring and named parameters
A fairly typical pattern in JavaScript is extracting values from objects. For example, it's very common to see functions that accept an options
object that modifies the way the function works. In ES5, this takes quite a bit of work. Consider the following snippet—it searches the DOM (document object model) for any elements matching a given query:
// Snippet Example 6b function findElements(options) { var first = false, query, els; if (options !== undefined && typeof options === "object") { if (options.first !== undefined) { first = options.first; } if (options.query !== undefined) { query = options.query; } } els = [].slice.call(document.querySelectorAll(query)); if (first === true) { els = [els[0]]; } return els; } console.log(findElements({ query: "a", first: true }).map(function(o) { return o.textContent; }));
Note
The snippets in this section are located at snippets/02/ex6-destructuring/a
...d
in the code package of this book. Using the interactive snippet playground, select 2: ES2015 and Browserify in the first dropdown and examples 6a … 6d in the second dropdown.
The first several lines are devoted to supplying appropriate defaults and extracting values out of the options
object. This is largely boilerplate; but if it is incorrect, bugs can arise. It would be so much better if we could avoid this entirely.
In ES2015, we can extract these values easily using a new feature called destructuring. It lets us write code like the following:
// Snippet Example 6a let {a, b} = { a: 5, b: 10 }; console.log(a, b); // 5 10
We can do this safely, even if the object doesn't contain one of the keys we're asking for—the value will simply be undefined
. Or, we can specify defaults:
let {a, b = 10} = { a: 5 }; // a = 5, b = 10
We can also drill into the object, as follows:
let {a: {b, c=10}} = { a: { b: 5 } }; console.log(b, c); // 5, 10, a is undefined
The same applies to arrays, as seen in the following snippet:
let [state, capitol] = ["Illinois", "Springfield"];
If there are more items than you care about, you can skip them:
let [a, , c] = [1, 2, 3]; console.log (a, c); // 1 3
You can use the ...
operator as well, should you wish to collect all the remaining values:
let [a, ...b] = [1, 2, 3]; console.log(a, b); // 1, [2, 3] let {c, ...d} = { c: 5, d: 10, e: 15 } console.log(c, d); // 5, {d: 10, e: 15}
Once we put all this together, we can rewrite our original code snippet as follows (in ES2015):
// Snippet Example 6c function findElements({query, first = false}) { let els = [].slice.call(document.querySelectorAll(query)); if (first === true) { els = [els[0]]; } return els; } console.log(findElements({ query: "a", first: true }).map(o => o.textContent));
Although it may not be immediately obvious, this also gives us another feature: named parameters. It's not quite as nice as it is in other languages that support named parameters, but it's good enough. It also means that our code is even more self-documenting.
We can use this to approximate multiple return values as well:
// Snippet Example 6d function doSomethingThatErrors() { let error = 5, message = "Element could not be found"; return { error, message }; } let {error, message} = doSomethingThatErrors(); console.log(error, message); // 5 Element could not be found
String interpolation
Many template and interpolation libraries have been developed for JavaScript, but ES2015 now provides built-in string interpolation.
Note
The snippets in this section are located at snippets/02/ex7-string-interpolation/a
in the code package of this book. Using the interactive snippet playground, select 2: ES2015 and Browserify in the first dropdown and example 7 in the second dropdown.
If you recall, we have already written an interpolation method named interpolate in the Variable Arguments section. For example, interpolate("My name is :1", "Bob")
would return My name is Bob
. In ES2015, we can achieve the same thing with the following:
let name = "Bob", str = `My name is ${name}`;
For most interpolation libraries, this is where it stops. But ES2015 allows us to evaluate arbitrary expressions:
let x = 2, y = 4, str = `X=${x},Y=${y},X+Y=${x + y}`; // X=2,Y=4,X+Y=6
Note
Anything that is an expression will work, including accessing object properties and methods.
As an added bonus, this new syntax also permits multiline strings:
let str = `SELECT * FROM FROM aTable`;
Of course, all whitespace is included, which means the string actually becomes "SELECT *\n FROM aTable"
. If spacing is important, you would need to take the necessary steps to strip the whitespace.
You can also define your own interpolations, which can be very useful. Let's consider a common security problem—SQL injection. For example, some developers are a bit lax when it comes to how they construct their SQL statements, as in the following example:
var sql = "SELECT * FROM customers WHERE customer_name = \"" + name + "\"";
Unfortunately, it's possible to inject arbitrary SQL code into this statement by supplying a specially crafted customer name. Now, before you think that ES2015 interpolation has saved you from security issue, be careful. The following construct is just as insecure:
let sql = `SELECT * FROM customers WHERE customer_name = ${name}`;
Tip
Do not ever write code like the prior code! Ever!
Instead, most libraries that work with SQL commands allow you to craft prepared statements. These statements are comprised of two parts: a SQL template and an array of variables that are slotted into their appropriate positions in the template. This sounds an awful lot like interpolation, and it is. The difference is that these libraries use the database's own code to properly handle values such that SQL injection becomes impossible.
It looks something like the following in practice:
var preparedStatement = { sql = "SELECT * FROM customers WHERE customer_name = ?", binds: ["Bob"] };
It's not terribly difficult to write SQL statements this way; but with ES2015, we can write our own interpolation handler that will make writing prepared statements even easier:
function sql(s, ...binds) { let len = binds.length; return { sql: s.map((val, idx) => (`${val}${idx < len ? "?" : ""}`)) .join(""), binds }; } let name = "Bob"; let preparedStatement = /*[1]*/ sql`SELECT * FROM customers WHERE customer_name = ${name}`; console.log(JSON.stringify(preparedStatement, null, 2)); // Console outputs // { // sql: SELECT * FROM customers WHERE customer_name= ? // binds: ["Bob"] // }
Take a look at [1]
in the prior code. This is ES2015's way of indicating that the sql
method is to be called in order to interpolate the string. Internally, this converts to sql(["SELECT * FROM CUSTOMERS W
HERE customer_name = ", ""], ["Bob"]);
.
Promises and a taste of ES2016
One often needs to write asynchronous code in JavaScript, especially when using Cordova plugins and also if one needs to make an XMLHttpRequest
. This means that you will typically end up writing code as follows:
// Snippet Example 8a function slowDiv(a, b, callback) { setTimeout(function() { if (b === 0) { callback("Can't pide by zero"); } else { callback(undefined, a / b); } }, 2500 + Math.random() * 10000); } slowDiv(10, 5, function(err, result) { if (err !== undefined) { console.log(err); return; } console.log(result); });
Note
The snippets in this section are located at snippets/02/ex8-promises/a
and snippets/02/ex8-promises/d
in the code package of this book. Using the interactive snippet playground, select 2: ES2015 and Browserify in the first dropdown and examples 8a-8d in the second dropdown
The previous example implements a very simple asynchronous method called slowDiv
which it returns the results of a pision operation after a short random period of time. If the pisor is zero, an error is returned instead.
The callback function accepts both the err
and result
parameters. If err
has some value other than undefined
, then we know that an error has occurred and we can handle it appropriately. Otherwise, we can log the result of the operation.
Note
The (err, result)
pattern is typical in Node.js applications.
Another similar callback pattern looks like the following – this is what you'll commonly see when using Cordova plugins:
// Snippet Example 8b function slowDiv(a, b, success, failure) { setTimeout(function() { if (b === 0) { failure("Can't pide by zero"); } else { success(a / b); } }, 2500 + Math.random() * 10000); } slowDiv(10, 5, function success(result) { console.log(result); }, function failure(err) { if (err !== undefined) { console.log(err); return; } });
The previous two code snippets do exactly the same thing—they return their result of pision very slowly. This doesn't look too bad as long as you only need to do it once. But if you need to chain pisions together, the resulting code will quickly become very ugly.
To help combat the "pyramid of doom" and the resulting difficulty in understanding the code, promises were developed. These allow you to write a function which promises that it will return a result at some point in the future. Promises can also be chained by calling the then
method on the resulting promise. Errors can be caught by supplying a catch
handler as well.
There have been many libraries that provide promises to JavaScript. ES2015, however, makes promises a part of the language. This doesn't necessarily obviate the need for other libraries (they may provide other interesting features), but it does mean that you can use promises without a library.
Note
A full primer on promises is beyond the scope of this chapter. If you want to learn more on promises, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise and https://www.promisejs.org.
With promises and other ES2015 syntax, we can rewrite the previous code as follows:
// Snippet Example 8c function slowDiv(a, b) { let p = new Promise((resolve, reject) => { setTimeout(() => { if (b === 0) { reject("Can't pide by zero"); } else { resolve(a / b); } }, 2500 + (Math.random() * 10000)); }); return p; } slowDiv(10, 5) .then(result => console.log(result)) .catch(err => console.log("error", err));
This is better than the example with callbacks; how much things improve will become obvious when we chain two slow pisions together, as follows:
slowDiv(10, 5) .then(result => slowDiv(20, result)) .then(result => console.log(result)) // 20 / ( 10 / 5 ) = 10 .catch(err => console.log("error", err));
As great as this is (and it's been heavily adopted by many JavaScript developers), it still doesn't feel like natural code. Instead it would be nice if we could write our asynchronous code as if it were synchronous.
With ES2015, we can't. We can come close with a combination of generators and a generator runner, but this still isn't quite natural. The next version of ECMAScript (ES2016), however, gives us a way to do almost exactly what we want: the await
and async
keywords.
Note
If you want to see how to simulate await
in ES2015, see http://davidwalsh.name/async-generators.
With these two keywords, we can now write our chained pision as follows:
async function doCalculation_a() { try { let result1 = await slowDiv(10, 5); // 2, eventually let result2 = await slowDiv(20, result1); // 10, eventually console.log(result2); } catch (err) { console.log("error", error); } }
There are a few things to be noted in the previous example. First, there's a new async
keyword (see [1]
)—this marks any function that returns an asynchronous result. Second, await
is used to mark where our asynchronous function is waiting for a value from a promise (see [2]
). Finally, it looks almost identical to the equivalent synchronous code! This means that we can reason how our code works even more easily.
There's only one problem: await
must exist within an async
function, and this means that any calling functions must also properly handle the asynchronicity. As such, you can't just use it wherever you want without planning for it. Even with this caveat, it's extremely useful.
Tip
The syntax for await
and async
has not yet been finalized. Unlike ES2015, which has been accepted by the standards body, it is possible that the syntax (and perhaps even the semantics) for this feature will change in the future. As such, be wary of using await
and async
in production code.
You can also use await
in expressions. So, we can write our doCalculation
method as follows:
async function doCalculation() { try { // 10, eventually console.log( await slowDiv(20, await slowDiv(10, 5))); } catch (err) { console.log("error", error); } }
How you use await
is largely up to you; the second example is a little harder to read, but it does the same thing.
Note
Using ES2016 features is considered experimental. As such, Babel won't recognize it by default. We'll address this in the Modifying our Gulp configuration section later in this chapter.
Classes
Classes are probably one of the more controversial features added to ES2015. But in reality, they are simply syntactic sugar for JavaScript's existing prototypical nature. You don't have to use ES2015 classes if you don't want to—you can still create objects the same way you did before. From a readability perspective, however, there are some definite benefits.
Note
The snippets in this section are located at snippets/02/ex9-classes/a
in the code package of this book. Using the interactive snippet playground, select 2: ES2015 and Browserify in the first dropdown and example 9 in the second dropdown.
Let's start off with a quick look at the new syntax. In the following snippet, we define a simple Point
class that represents an x and y coordinate on a two-dimensional plane:
class Point { constructor(x = 0, y = 0) { // [1] [this._x, this._y] = [x, y]; } get x() { // [2] return this._x; } set x(x) { // [3] this._x = x; } get y() { return this._y; } set y(y) { this._y = y; } toString() { return `(${this.x}, ${this.y})`; } copy() { return new Point(this.x, this.y); } equal(p) { return (p.x === this.x) && (p.y === this.y); } }
Notice that we defined a constructor using the constructor
keyword ([1]
). Property getters and setters are defined using get
([2]
) and set
([3]
), respectively. Now, let's imagine that we want to create a rectangle. We could simply specify the top left and the bottom right points; let's allow the developer to specify the size instead. Although we could make the size a point
, it would be more convenient to use w
and h
instead of x
and y
. That's easy to accomplish, as seen in this snippet:
class Size extends Point { // [1] constructor(w = 0, h = 0) { super(w, h); } get w() { return this.x; } set w(w) { this.x = w; } get h() { return this.y; } set h(h) { this.h = h; } copy() { return new Size(this.w, this.h); } }
Notice the new extends
keyword ([1]
)—this allows you to indicate that one class is based on another. The new class gains all the previous properties and methods automatically. Better yet, the subclass can call its parent's methods using super
.
Now we can create a Rectangle
class:
class Rectangle { constructor(origin = (new Point(0, 0)), size = (new Size(0, 0))) { [this._origin, this.size] = [origin.copy(), size.copy()]; } get origin() { return this._origin; } set origin(origin) { this._origin = origin.copy(); } get size() { return this._size; } set size(size) { this._size = size.copy(); } copy() { return new Rectangle(this.origin, this.size); } toString() { return `${this.origin}:${this.size}`; } equal(r) { return this.origin.equal(r.origin) && this.size.equal(r.size); } equalSize(r) { return this.size.equal(r.size); } }
We can use all of these objects just as we expect:
let pointA = new Point(10, 50); let sizeA = new Size(25, 50); let rectA = new Rectangle(pointA, sizeA); console.log(`${rectA}`); // (10,50):(25,50)
Note
When objects are used with string interpolation, toString
is called automatically.
Modules
The final new ES2015 feature that we'll cover in this section is modules. There have been many ways to approach modularization in JavaScript, but ES2015 brings modularization into the language itself. The syntax is pretty simple; but if you aren't familiar with modules, it might take a while for you to absorb it fully.
In ES2015, a module is a separate JavaScript file that has one or more methods or variables marked with export
. This is a new keyword that allows you to indicate that a value or method should be available to the other parts of your program. You can choose to provide a default export using export default
.
Conversely, your code consumes a module with import
. You can choose to import everything that the module exports with import *
or you can pick and choose.
Let's consider an example. First, let's define a simple module:
// math.js export function add(a, b) { return a + b; } export function sub(a, b) { return a - b; }
We can use this module in another JavaScript file, as follows:
// another.js import * from "math.js"; // the above line is equivalent to the following // import {add, sub} from "math.js" console.log(add(2, 5));
Now, imagine that we want to make an advanced math library, but we want to pass on the simpler methods to the consumer. We can export methods and values directly from another file, as follows:
// advancedMath.js export * from "math.js" export function mul(a, b) { return a * b; }
None of this is terribly earth shattering—most developers use something similar, but it is nice to have it baked in. Furthermore, it does provide a little more flexibility, since you can pick and choose what you want to import and export.
Unfortunately, you need a packaging utility that provides support for this feature—it won't work by default with Babel. We'll cover that in the Using Browserify section later in this chapter.
More information
You've read a lot of information, but it's going to be critical to understand while reading the code in the rest of this book. Take some time to familiarize yourself with the information provided in this chapter. Then, I suggest that you read up some of the other features we didn't go over. The following websites provide a good starting point:
Tip
Not every ES2015 feature is supported by the transpiler that we are using, namely Babel. For specifics on what Babel supports, see http://kangax.github.io/compat-table/es6/.
In order to test your comprehension of all the ES2015 features, or if you just want to play around, Babel has an excellent sandbox, where you can write ES2015 code, see the results of the transpilation, and then verify that whether the code works. You can access it online at https://babeljs.io/repl/.