Use ES2015+
now, not later!

A talk by Christophe Porteneuve at Voxxed Days Thessaloniki 2016

whoami


const christophe = {
  ηλικία:      38.9637234770705,
  οικογένεια:  { γυναίκα: 'Élodie', υιός: 'Maxence' },
  τοποθεσία:   'Paris, FR',
  εταιρεία:    'Delicious Insights',
  σεμινάρια:   ['360° JS', 'Node.js', '360° Git'],
  στοWebΑπό:   1995,
  ΔιασημοςΓια: [
    'Prototype.js',
    'Ruby On Rails',
    'Prototype and Script.aculo.us',
    'Paris Web'
  ]
}
          

From ES3 to ES2016

May 1995 JavaScript 1.0 (in 10 days). Netscape 2.0ß3 on 12/95.
June 1997 ECMAScript (ES1, ECMA-262), official standard.
Dec. 1999 ES3. JScript 1.5 about on par (IE4–8)
Dec. 2009 (!) ES5. Compatibility baseline for now. IE9+, Node, etc. Few big changes.
June 2015 ES2015 / ES6. Huge changes to the language itself.
June 2016 ES2016 / ES7. Few big changes. But wait for ES2017!

Native support?

Great compat table by Juriy “Kangax” Zaytsev.

Browsers

Evergreens 90–98%. Safari 10+ too.

Runtimes

Node 4 = 48%, Node 5 = 58%, Node LTS 6 = 96%!

Yeah, but…?

Sure, you may need to deal with IE (9–11), and even 58% isn’t quite enough to be serene. So what do we do then?

Babel

Transpiles ES2015+1 (ES2015, ES2016, ES2017…) to ES5

This way, if you have ES5, you’re golden

In practice: IE9+, evergreens, all Node.js / io.js

Integrates with everything

1 Almost everything… not proxies.

ES2015, why?

Easier

More powerful

More reliable

More performant

More fun

Objects & Classes

So much simpler

Objet literals


res.render('entries/index', {
  pageTitle: 'Bookmarks', entries, entryCount, tags
})
            

const coreReducer = combineReducers({
  currentUser, goals, history, today, todaysProgress
})
            

return { [action.goalId]: previous + increment }
            

Classes


class TodoItem extends Component {
  constructor (props, context) {
    super(props, context)
    this.state = {
      editing: false
    }
  }

  handleDoubleClick () {
    this.setState({ editing: true })
  }
  …
}
            

Classes (cont’d)


class TodoItem extends Component {
  static MINIMUM_COMPLETION_TIME = 5

  get completionTime () {
    return (this.checkedAt - this.startedAt) / 1000
  }

  set completionTime (value) {
    this.checkedAt = this.startedAt + Math.max(value, MINIMUM_COMPLETION_TIME) * 1000
  }

  cleanUp () {
    super.cleanUp()
    this.customState = null
  }
}
            

Huge extra comfort

for lots of little things

Destructuring


const { filter: selectedFilter, onShow } = this.props
            

const [, filters] = output.props.children
            

var { op: a, lhs: { op: b }, rhs: c } = getASTNode()
            

function convertNode ({ nodeType, nodeValue }) {
  switch (nodeType) {
    case ELEMENT_NODE:
      convertElementNode(nodeValue)
      break
    // …
  }
}
            

Destructuring


const [
  endpoint = '/users/me',
  output = 'response.json'
] = process.argv.slice(2)
            

const HistDayGoal = ({ goal: { name, units }, stats: [progress, target] }) => {
  // …
}
            

Promise.all([
  Entry.tags(), Entry.count().exec(), Entry.getEntries(req.query)
])
.then(([tags, entryCount, entries]) => {
  res.render('entries/index', { title: 'Bookmarks', entries, entryCount, tags })
})
              

Rest & Spread


function winners(boss, realBoss, ...contenders) {
  console.log(boss, realBoss, contenders)
}
winners('Ζεύς', 'Ἥρα', 'Μῆτις', 'Θέμις', 'Λητώ')
// => 'Ζεύς', 'Ἥρα', ['Μῆτις', 'Θέμις', 'Λητώ']

var arr1 = ['one', 'two'], arr2 = ['three', 'four']
arr1.push(...arr2) // => 4, arr1 is ['one', 'two', 'three', 'four']

var helen = { rel: 'Μενέλαος', loc: 'Σπάρτη', mother: 'Λήδα', stable: true }
var kidnapping = { rel: 'Πάρις', loc: 'Τροία' }
var trouble = { ...helen, ...kidnapping, stable: false }
// => { rel: 'Πάρις', loc: 'Τροία', mother: 'Λήδα', stable: false }

var nodeListAsArray = [...document.querySelectorAll('h1')]
            

return [{ id: …, completed: …, text: … }, ...state]
            

Default values


function todos (state = initialState, action) {
  // …
}

function convertNode ({ nodeType = ELEMENT_NODE, nodeValue }) {
  // …
}
            

var [first, second = 'foo'] = ['yo']
// second === 'foo'

var person = { first: 'Louis', sex: 'M', age: 36 }
var { first, last = 'Doe', age = 42 } = person
// first === 'Louis', last === 'Doe', age === 36
            

Template strings


var person = { first: 'Thomas', last: 'Anderson', age: 25, nickname: 'Neo' }

// Any-expr interpolation
console.log(`${person.first} aka ${person.nickname}`)
// => 'Thomas aka Neo'

// Multiline!
var markup = `<li>
  ${person.first} ${person.last}, age ${person.age}
</li>`
            

let & const


const { activeCount } = this.props
…
const { filter: selectedFilter, onShow } = this.props
            

const ADD_TODO = 'ADD_TODO'
const DELETE_TODO = 'DELETE_TODO'
const EDIT_TODO = 'EDIT_TODO'
            

Scope: the block (even implicit loop block)


for (let index = 0; index < 10; ++index)
  setTimeout(() => console.log(index), 10)
  // => Indeed 0 through 9, not 10 x 10, despite delayed closure
            

const is the new var. let only if need proven.

const ≠ immutable

Warning! const isn’t recursive/deep freezing!


const author = { name: 'Bob', language: 'js' }
author.language = 'es6'

const days = ['Tuesday', 'Wednesday', 'Thursday']
days.push('Friday')
            

To deep-freeze, use deep-freeze.

Extended literals

Octal and binary (052 wouldn’t work in strict mode anyway)


0o52     // => 42
0b101010 // => 42
            

MOAR Unicode


"𠮷".length == 2                      // Char count, not codepoint count

"\u{20BB7}" == "𠮷" == "\uD842\uDFB7" // New escape syntax for full codepoints

"𠮷".match(/./u)[0].length == 2       // New 'u' flag for regexps, changes classes

"𠮷".codePointAt(0) == 0x20BB7        // New codePointAt API (adds to charCodeAt)
            

Arrow functions

Doesn’t redefine this*, so it stays lexical!


{goals.map((goal) =>
<GoalSetting key={goal.id} goal={goal}
  onDeleteClick={() => this.openGoalDeleter(goal)}
  onEditClick={() => this.openGoalEditor(goal)}
/>
)}
            

const mapStateToProps = ({ goals, currentUser }) => ({ goals, currentUser })
            

Ideal for predicates, filters, transforms…


[12, 18, 21, 23].map((hour) => [hour, 0, 0])
            

return state.map((goal) => goal.id === newGoal.id ? newGoal : goal)
            
* Nor arguments, super and new.target. Can’t be used as a constructor or bound • Details

Maps & Sets

2 new types of collections.


const s = new Set()
s.add('es2015').add('trashes').add('es5').add('es2015')
s.size          // => 3
s.has('es2015') // => true

const items = ['es2015', 'trashes', 'es5', 'es2015']
[...new Set(items)] // => ['es2015', 'trashes', 'es5']
            

const m = new Map()
m.set('42', 'foobar')
m.get('42') // => 'foobar'
m.get(42)   // => undefined
m.has(42)   // => false
m.size      // => 1
            

Asynchrony

is easy

Promises ≠ Callbacks++

Three main strengths

Garantees: unique, exclusive call (success/failure)

Exception capturing

Composability

Widespread

Tons of libs pre-ES2015, jqXHR, recent Web APIs…
(ServiceWorker, Fetch, Streams, Push, Notification, Battery, etc.)

Promises: then


somePromise.then(value => processing)

// Processing can return a fixed value: turned into a fulfilled promise…
getJSON('/meta').then((res) => res.user_url)
// …or return another promise, inserted in the chain
getJSON('/').then((res) => getJSON(res.user_url)).then(console.log)
            

Actually 2 arguments: callbacks for success and failure


getJSON('/meta')
  .then((res) => getJSON(res.user_url))
  .then(console.log, console.error)
  .then(releaseConnection)
            

As soon as an error is consumed, it’s neutralized
(just like with try…catch)

Promises: catch

Beware: same-level callbacks are mutually exclusive.


getJSON(res.user_url).then(
  (res) => res.fouleNayme.split('/'), // Blam!
  console.error // Never called
)

getJSON(res.user_url).then((res) => res.fouleNayme.split('/')) // Blam!
  .then(console.log, console.error) // console.error called!
            

getJSON(res.user_url).then((res) => res.fouleNayme.split('/'))
  .then(console.log)
  .catch(console.error)
            

A great playground: Promisees

async / await


async function chainAnimationsAsync (elem, animations) {
  let ret = null
  try {
    for (const anim of animations) {
      ret = await anim(elem)
    }
  } catch (e) { /* ignore and keep going */ }
  return ret
}
            

async / await


async function processUserNotebook (request, response) {
  try {
    const user = await User.get(request.user)
    const notebook = await Notebook.get(user.notebook)
    response.send(await doSomethingAsync(user, notebook))
  } catch (err) {
    response.send(err)
  }
}
            

async / await


async function listEntries (request, response) {
  const [tags, entryCount, entries] = await Promise.all([
    Entry.tags(), Entry.count().exec(), Entry.getEntries(req.query)
  ])
  res.render('entries/index', { title: 'Bookmarks', entries, entryCount, tags })
}
            

Actual modules

that rock

Exporting stuff

Langage level; no circular dependencies thanks to live bindings.


// Explicit, on-the-fly named exports
export function addTodo (text) {
  return { type: types.ADD_TODO, text }
}

export const ADD_TODO = 'ADD_TODO'

// Default export (at most one per module)
export default Footer

// Post-declaration exports
export { Component, Factory, makeHigherOrder }
                    

// Renamed exports
export { each as forEach }

// Re-exporting (delegation)
export * from './lib/cool-module'

// or, more narrow:
export {
  makeHigherOrder as wrap,
  Component
} from 'toolkit'
                    

Importing stuff


// Full import, Node namespace style:
import * as types from '../constants/ActionTypes'

// Named import of the default export, plus homonymous named exports
import React, { PropTypes, Component } from 'react'

// Or just one case at a time:
import classnames from 'classnames'
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'

// Renamed import:
import { makeHigherOrder as wrap } from 'toolkit'
            

Asynchronous loading

An asynchronous loading API for when we need it…


System.import('toolkit')
  .then(toolkit => …)
  .catch(error => …)

// Many modules?  Combine promises!
Promise.all(['toolkit', 'security', 'advanced'].map(System.import))
.then(([toolkit, security, advanced]) => …)
.catch(error => …)
            

Meta
programming

I see what you did there

Proxies

Possible intercepts of any interaction with an object.

  • Properties (hence methods): read, write, define, delete, test…
  • Functions: construction with new, call…
  • Prototypes: get, set…

You can do anything!… but non-emulable with Babel.
Edge 12+, Chrome 49+, Firefox 38+, Node 6+.

Proxies

The gist: you build a proxy that wraps a target object.

You define traps on it for every intercept you need. The Reflect API provides built-in, default implementations.


function wrapAsDefensive (obj) {
  return new Proxy(obj, {
    get (target, property, receiver) {
      if (!(property in target)) {
        throw new ReferenceError(
          `Property ${property} missing in ${JSON.stringify(target)}`)
      }

      return Reflect.get(target, property, receiver)
    }
  })
}
            

Decorators

ES.Next facilitates higher-order components with this. Beware though, back to stage 1 (drawing board)…


  @autobind
  class TodoItem extends Component {
    @memoize('5min')
    getOverallCounts () {
      // …
    }

    @override
    render () {
      // …
    }
  }
            

Exploring ES2015+ further

ES6-Features.org

ES6 Katas

ES6 In Depth (MDN)

ES6 In Depth (Nicolás “Ponyfoo” Bevacqua)

Exploring ES6 (Dr. Axel Rauschmayer)

Understanding ES6 (Nicholas Zakas)

lebab (née xto6)

ευχαριστώ!

And may JS be with you


Christophe Porteneuve

@porteneuve

Slides are at bit.ly/voxxed-es2015