- Understand what ES2015 is and how to use it
- Exposure to many of the most useful new features introduced in ES2015
- Update an existing react app to use ES2015 features
ES2015? ES2015 (sometimes called ES6, ESHarmony) is the new version of Javascript that was released in June 2015. It adds a ton of new syntax and functionality aimed at making writing complex applications easier
Cool! Can I use it? The short answer is yes! (cross your fingers).
Check out this awesome table of features and where they are supported.
TL;DR: Decent desktop support of Firefox, Edge (MS finally ditched IE and made something awesome), Chrome (with 'use strict'). Somethings work on Node, Safari is still 'meh' and there is limited ES6 support on mobile browsers.
The current solution to improve compatibility is to transpile your ES6 code to ES5 using something like Babel or Traceur. I use Babel because at the moment it has slightly better feature support and an awesome REPL.
You are going to be using these features to refactor an existing app. The app we're using today is React OMDB, which was also used in the the second react lesson.
Steps:
- Clone down react-omdb if you don't already have it
cd
into it- Switch to the es6-starter branch
$ git checkout es6-starter
- Make sure current dependencies are installed
$ npm install
- Install additional babel dev dependencies
$ npm install --save-dev babel-preset-es2015
- Open the repo in atom
$ atom .
- Add the new babel preset to your
.babelrc
file
{
"presets": [
"react",
"babel-preset-es2015"
]
}
Start the webpack-dev-server by running npm start
and go to 127.0.0.1:8080.
Spend a few minutes playing trying to figure out how the app works. Play with
the app in the browser and looking at the code in atom responsible for it.
You're probably familiar with this mess:
// ES5
"<h1>❤ unicorns</h1>\n" +
"<p>Unicorn pegasus pony rainbows pegasus pony kittens. Pop pigeon rainbows pony delight kittens kittens surprise. Wereunicorn delight pony pony social unicorn surprise.</p>\n" +
"<ol>\n" +
"<li>Yea! Yeah!</li>\n" +
"<li>Yeah, woo-hoo!</li>\n" +
"</ol>"
In ES2015 you can place strings between back-ticks (the symbol below the esc
key) to use multi-line strings
// ES2015
`<h1>❤ unicorns</h1>
<p>Unicorn pegasus pony rainbows pegasus pony kittens. Pop pigeon rainbows pony delight kittens kittens surprise. Wereunicorn delight pony pony social unicorn surprise.</p>
<ol>
<li>Yea! Yeah!</li>
<li>Yeah, woo-hoo!</li>
</ol>`
Another painful thing in Javascript is string interpolation
// ES5
var name = "bob", age = 71;
"Hello my name is "+ name +" and I'm "+ age +" years old!"
ES2015 introduces template literals which make the process much less painful. Back-ticks are also required and this also supports multi-line strings.
// ES2015
var name = "bob", age = 71;
`Hellow my name is ${name} and I'm ${age} years old!`
Notice how similar this looks to JSX? That's no coincidence! JSX leverages this with a little extra syntactic sugar to allow us to write code that looks more like html in our javascript. This is also why we need babel to use JSX.
let
is the new var
The var
statement declares a variable globally, or locally to an entire function, optionally initializing it to a value.
// ES5
function varTest() {
var x = 31;
if (true) {
var x = 71; // same variable!
console.log(x); // 71
}
console.log(x); // 71
}
The let
statement declares a block scope local variable, optionally initializing it to a value.
// ES2015
function letTest() {
let x = 31;
if (true) {
let x = 71; // different variable
console.log(x); // 71
}
console.log(x); // 31
}
Another example:
"use strict";
let a = [];
(function () {
for (let i = 0; i < 5; ++i) { // *** `let` works as expected ***
a.push( function() {return i;} );
}
} ());
console.log(a.map( function(f) {return f();} ));
// prints [0, 1, 2, 3, 4]
// Start over, but change `let` to `var`.
// prints [5, 5, 5, 5, 5]
Everything let
does const
also does, except that const
can't be reassigned. The const declaration creates a read-only reference to a value. It does not mean the value it holds is immutable. Const requires an initial value.
// ES5
// ES2015
Caveats
Replace var with let and const
'Concise Object Methods' are a new syntax for writing functions with objects that allow you to skip writing ': function'
// ES5
var doStuff = {
sayHello: function() {
console.log("Hello");
},
eatPizza: function(pizza) {
pizza = 0;
return "thanks!"
}
}
// ES2015
let doStuff = {
sayHello() {
console.log("Hello");
},
eatPizza(pizza) {
pizza = 0;
return "thanks!"
}
}
Convert all object methods to use new syntax by removing : function
According to MDN the two biggest motivations for arrows were shorter functions and lexical this
. Arrows can make your code cleaner and more intuitive.
- Go to the Babel REPL.
- Paste in the code below:
var add = (x , y) => x + y;
What did you notice? Compare the syntax between ES6 (left) and ES5 (right).
Arrows save having to write function
& return
for simple functions like this. When only one variable is used, the parenthesis can also be omitted x => x * x
, I always keep the parens for consistency.
Arrows can be used as IIFEs. To return object literals the object needs to be wrapped in parenthesis. Arrows are anonymous by default, or can be assigned to a variable just like normal functions.
- Paste the code below into the Babel REPL.
(() => "I'm an IIFE")() //IIFE
var foobar = () => ({foo: 'bar'}) // Object Literal
Solution / Easy reference - Arrow's IIFE and returning object literals syntax transpiled to ES5.
Arrows can also be used with block statements It should be noted that in blocks the return
is not automatic and needs to be explicitly stated. This still saves having to write function
.
- Paste the code below into the Babel REPL.
$('#pizza-btn').click( (event) => {
preheatOven();
pizzaInOven();
return 'I love za!'
});
Solution / Easy reference - ES6 arrows with blocks transpiled to ES5:
Shorter syntax is awesome, especially because I'm always misspelling funciton
.
The rest of the section assumes you are comfortable with scope, context, and closures in javascript. Detailed reviews of these concepts can be found here, here, and here. The TL;DR: Context refers to the object that the currently executing function is attached to - determined in most cases by how a function is called. The value of this
references whatever the current context is. Scope is where a variable can be referenced (where it's defined).
Where arrows really shine is by binding this
. The this
in an arrow is set to the this value of the enclosing execution context.
Here we have a function, in this case an Object constructor function. Inside our function is a method that executes a callback function. We want the ovenTemp of the Oven to increase every 1000 ms.
function Oven () {
this.ovenTemp = 0; // here `this` references instance of Oven constructor
setInterval(function preheatOven() {
// preheatOven() redefines context of `this`
this.ovenTemp++; // here `this` references the global object, undesired action
}, 1000);
}
What does this
refer to in both spots?
- The Oven.ovenTemp doesn't change because our callback changed the context of
this
.
How can we fix the problem?
- One way we can remedy the problem by binding
this
.
function Oven () {
this.ovenTemp = 0; // here `this` references instance of Oven constructor
setInterval(function preheatOven() {
this.ovenTemp++;
}, 1000).bind(this);
}
Another, more performant, way to fix the problem is to store the top level this
reference/context in a variable, commonly seen as self = this
or that = this
.
function Oven () {
var self = this; // stores top level `this` reference
self.ovenTemp = 0;
setInterval(function preheatOven() {
self.ovenTemp++; // performs desired action
}, 1000);
}
A better solution is to leverage the lexical scoping of arrow functions.
function Oven () {
this.ovenTemp = 0;
setInterval( preheatOven = () => {
this.ovenTemp++; // here `this` references instance of Oven constructor, as desired
}, 1000);
}
In this particular example we can use an anonymous callback and refactor to one line. Wow that's pretty!
function Oven () {
this.ovenTemp = 0;
setInterval( () => { this.ovenTemp++; }, 1000);
}
This trivial example shows a non trivial use case - arrows can simplify callbacks and/or closures and allow for more intuitive use of context.
Convert remaining functions to arrow syntax
Destructing is a new syntax that makes it easier to extract data from objects and arrays. Destructing matches patterns to assign values to variables.
// ES2015
let [a, b] = ["cats", "dogs"]
console.log(a); // "cats"
console.log(b); // "dogs"
let x = {y: "pizza", z: 100}
let {y, z} = x
console.log(y); // "pizza"
console.log(z); // 100
For destructing objects the assigned name needs to match the keys of the object
// ES2015
let x = {y: "pizza", z: 100}
let {foo, bar} = x // not useful: neither foo or bar are in 'x'
console.log(foo); // undefined
console.log(bar); // undefined
- nested object and array destructing
- combine with default parameters
- combine with default spread
- combine with iteration
Modules are just self contained sections of code. They make it easier to break a big system into smaller pieces. Prior to ES2015 there was no standard way of creating modules, some patterns use IIFEs, CommonJS syntax (the one used in node), AMD modules, plus more.
Here's an example of the CommonJS syntax you have seen in node:
// ES5
// someFile
function sayHello() {
console.log("Hello");
}
var cats = "cats"
module.exports = {
cats: cats,
sayHello: sayHello
};
////////////////////
// differentFile
var pizza = require("whatever/path/to/someFile");
pizza.sayHello()
// > "Hello"
You can export multiple items and are able to specify exactly what you want to import.
// ES2015
// someFile
export function sayHello() {
console.log("Hello");
}
export const cats = "cats"
////////////////////
// differentFile
import sayHello from "whatever/path/to/someFile"
sayHello()
// > "Hello"
- importing everything from a module
- default exports and imports
- aliasing imports
Convert imports to ES2015 syntax
Solution (disregard the updates to .babelrc and package.json, you should have already done that)
// ES5
function Pizza(name, temperature) {
this.name = name;
this.temperature = temperature
}
Pizza.prototype.heatUp = function () {
return this.temperature + 20;
};
Classes provided syntactic sugar over JavaScript's existing prototype-based inheritance. A class body can only methods and not data properties. Putting data in prototypes is generally an anti-pattern, so this enforces a best practice. Data is instead attached to classes using the constructor
method. Instance methods (defined with concise object syntax) are automatically connected through prototypical inheritance.
// ES2015
class Pizza {
constructor(name, temperature) {
this.name = name;
this.temperature = temperature;
}
static sayCool() {
console.log("cool!");
}
heatUp() {
return this.temperature + 20;
}
}
The static
keyword defines a static method for a class. Static methods are called without instantiating their class and are also not callable when the class is instantiated. Static methods are often used to create utility functions for an application.
MDN static
// ES2015
Pizza.sayCool();
// > "cool!"
let za = new Pizza("cheese", "hot")
za.sayCool();
// TypeError: za.sayCool is not a function
The extends
keyword is used in a class declaration (or class expression) to create a class with a child of another class. MDN extends
The super
keyword is used to call functions on an objects parent. MDN extends
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes http://www.2ality.com/2015/02/es6-classes-final.html
// ES2015
class JumboSlice extends Pizza {
constructor(name, temperature) {
super(cheese, lavaHot); // calls the constructor of the class being extended
this.size = "jumbo";
}
cheesyRegret() {
return "penitent stomach"
}
}
Update to use classes
We will no longer be using React.createClass
and are instead creating classes with the new syntax that extend React.Component
.
- For the components that use
const <ComponentName> = React.createClass({
and just containrender() {
update them toclass <ComponentName> extends React.Component{
. This also involves removing the parenthesis that came fromcreateClass()
React.createClass
automatically boundthis
to the methods to keep context when the methods are called we can use arrow functions:
- Update
onUpdateSearch={this.handleUpdateSearch}
onSubmitSearch={this.handleSubmitSearch}
to:
onUpdateSearch={(event) => this.handleUpdateSearch(event)}
onSubmitSearch={(event) => this.handleSubmitSearch(event)}
- Update the
SearchContainer
component to also use class syntax.
- Remove the commas between methods
- change
getInitialState()
toconstructor()
- include
super()
to call the constructor of React.Component (the thing we're extending)
- getInitialState() {
- return {
+ constructor() {
+ super();
+ this.state = {
- class expressions vs class declarations
- getters and setters
- mixins
- class hoisting
Math, Number, String, Array, Object
A symbol is a unique and immutable data type and may be used as an identifier for object properties. The Symbol object is an implicit object wrapper for the symbol primitive data type.
Default function parameters allow formal parameters to be initialized with default values if no value or undefined is passed.
// ES2015
function multiply(a, b = 2) {
return a*b;
}
multiply(5); // 10
var parts = ['shoulders', 'knees'];
var lyrics = ['head', ...parts, 'and', 'toes']; // ["head", "shoulders", "knees", "and", "toes"]
The rest parameter syntax allows us to represent an indefinite number of arguments as an array. If the last named argument of a function is prefixed with ...
, it becomes an array with the remaining arguments passed to the function.
// ES2015
function(a, b, ...theArgs) {
// ...
}
Offical support for promises with an api that is similar to ones that already exist in many libraries.
// ES2015
function timeout(duration = 0) {
return new Promise((resolve, reject) => {
setTimeout(resolve, duration);
})
}
var p = timeout(1000).then(() => {
return timeout(2000);
}).then(() => {
throw new Error("hmm");
}).catch(err => {
return Promise.all([timeout(100), timeout(200)]);
})
// ES2015
let iterable = [10, 20, 30];
for (let value of iterable) {
console.log(value);
}
// 10
// 20
// 30
Data structures that make it easier to complicated things.
// ES2015
// Sets
var s = new Set();
s.add("hello").add("goodbye").add("hello");
s.size === 2;
s.has("hello") === true;
// Maps
var m = new Map();
m.set("hello", 42);
m.set(s, 34);
m.get(s) == 34;
// Weak Maps
var wm = new WeakMap();
wm.set(s, { extra: 42 });
wm.size === undefined
// Weak Sets
var ws = new WeakSet();
ws.add({ data: 42 });
Map / Set / WeakMap / WeakSet Example
Make it easier to use function*
and yield
The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).