Mastering PhoneGap Mobile Application Development
上QQ阅读APP看书,第一时间看更新

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/.