ES6 Overview in 350 Bullet Points
My ES6 in Depth series consists of 24 articles covering most syntax changes and features coming in ES6. This article aims to summarize all of those, providing you with practical insight into most of ES6, so that you can quickly get started. I've also linked to the articles in ES6 in Depth so that you can easily go deeper on any topic you're interested in.
I heard you like bullet points, so I made an article containing hundreds of those bad boys. To kick things off, here's a table of contents with all the topics covered. It has bullet points in it -- obviously. Note that if you want these concepts to permeate your brain, you'll have a much better time learning the subject by going through the in-depth series and playing around, experimenting with ES6 code yourself.
Table of Contents
- Introduction
- Tooling
- Assignment Destructuring
- Spread Operator and Rest Parameters
- Arrow Functions
- Template Literals
- Object Literals
- Classes
- Let and Const
- Symbols
- Iterators
- Generators
- Promises
- Maps
- WeakMaps
- Sets
- WeakSets
- Proxies
- Reflection
Number
Math
Array
Object
- Strings and Unicode
- Modules
Apologies about that long table of contents, and here we go.
Introduction
- ES6 -- also known as Harmony,
es-next
, ES2015 -- is the latest finalized specification of the language - The ES6 specification was finalized in June 2015, (hence ES2015)
- Future versions of the specification will follow the
ES[YYYY]
pattern, e.g ES2016 for ES7- Yearly release schedule, features that don't make the cut take the next train
- Since ES6 pre-dates that decision, most of us still call it ES6
- Starting with ES2016 (ES7), we should start using the
ES[YYYY]
pattern to refer to newer versions - Top reason for naming scheme is to pressure browser vendors into quickly implementing newest features
Tooling
- To get ES6 working today, you need a JavaScript-to-JavaScript transpiler
- Transpilers are here to stay
- They allow you to compile code in the latest version into older versions of the language
- As browser support gets better, we'll transpile ES2016 and ES2017 into ES6 and beyond
- We'll need better source mapping functionality
- They're the most reliable way to run ES6 source code in production today (although browsers get ES5)
- Babel (a transpiler) has a killer feature: human-readable output
- Use
babel
to transpile ES6 into ES5 for static builds - Use
babelify
to incorporatebabel
into your Gulp, Grunt, ornpm run
build process - Use Node.js
v4.x.x
or greater as they have decent ES6 support baked in, thanks tov8
- Use
babel-node
with any version ofnode
, as it transpiles modules into ES5 - Babel has a thriving ecosystem that already supports some of ES2016 and has plugin support
- Read A Brief History of ES6 Tooling
Assignment Destructuring
var {foo} = pony
is equivalent tovar foo = pony.foo
var {foo: baz} = pony
is equivalent tovar baz = pony.foo
- You can provide default values,
var {foo='bar'} = baz
yieldsfoo: 'bar'
ifbaz.foo
isundefined
- You can pull as many properties as you like, aliased or not
var {foo, bar: baz} = {foo: 0, bar: 1}
gets youfoo: 0
andbaz: 1
- You can go deeper.
var {foo: {bar}} = { foo: { bar: 'baz' } }
gets youbar: 'baz'
- You can alias that too.
var {foo: {bar: deep}} = { foo: { bar: 'baz' } }
gets youdeep: 'baz'
- Properties that aren't found yield
undefined
as usual, e.g:var {foo} = {}
- Deeply nested properties that aren't found yield an error, e.g:
var {foo: {bar}} = {}
- It also works for arrays,
var [a, b] = [0, 1]
yieldsa: 0
andb: 1
- You can skip items in an array,
var [a, , b] = [0, 1, 2]
, gettinga: 0
andb: 2
- You can swap without an "aux" variable,
[a, b] = [b, a]
- You can also use destructuring in function parameters
- Assign default values like
function foo (bar=2) {}
- Those defaults can be objects, too
function foo (bar={ a: 1, b: 2 }) {}
- Destructure
bar
completely, likefunction foo ({ a=1, b=2 }) {}
- Default to an empty object if nothing is provided, like
function foo ({ a=1, b=2 } = {}) {}
- Assign default values like
- Read ES6 JavaScript Destructuring in Depth
Spread Operator and Rest Parameters
- Rest parameters is a better
arguments
- You declare it in the method signature like
function foo (...everything) {}
everything
is an array with all parameters passed tofoo
- You can name a few parameters before
...everything
, likefunction foo (bar, ...rest) {}
- Named parameters are excluded from
...rest
...rest
must be the last parameter in the list
- You declare it in the method signature like
- Spread operator is better than magic, also denoted with
...
syntax- Avoids
.apply
when calling methods,fn(...[1, 2, 3])
is equivalent tofn(1, 2, 3)
- Easier concatenation
[1, 2, ...[3, 4, 5], 6, 7]
- Casts array-likes or iterables into an array, e.g
[...document.querySelectorAll('img')]
- Useful when destructuring too,
[a, , ...rest] = [1, 2, 3, 4, 5]
yieldsa: 1
andrest: [3, 4, 5]
- Makes
new
+.apply
effortless,new Date(...[2015, 31, 8])
- Avoids
- Read ES6 Spread and Butter in Depth
Arrow Functions
- Terse way to declare a function like
param => returnValue
- Useful when doing functional stuff like
[1, 2].map(x => x * 2)
- Several flavors are available, might take you some getting used to
p1 => expr
is okay for a single parameterp1 => expr
has an implicitreturn
statement for the providedexpr
expression- To return an object implicitly, wrap it in parenthesis
() => ({ foo: 'bar' })
or you'll get an error - Parenthesis are demanded when you have zero, two, or more parameters,
() => expr
or(p1, p2) => expr
- Brackets in the right-hand side represent a code block that can have multiple statements,
() => {}
- When using a code block, there's no implicit
return
, you'll have to provide it --() => { return 'foo' }
- You can't name arrow functions statically, but runtimes are now much better at inferring names for most methods
- Arrow functions are bound to their lexical scope
this
is the samethis
context as in the parent scopethis
can't be modified with.call
,.apply
, or similar "reflection"-type methodsarguments
is also lexically scoped to the nearest normal function; use(...args)
for local arguments
- Read ES6 Arrow Functions in Depth
Template Literals
- You can declare strings with
`
(backticks), in addition to"
and'
- Strings wrapped in backticks are template literals
- Template literals can be multiline
- Template literals allow interpolation like
`ponyfoo.com is ${rating}`
whererating
is a variable - You can use any valid JavaScript expressions in the interpolation, such as
`${2 * 3}`
or`${foo()}`
- You can use tagged templates to change how expressions are interpolated
- Add a
fn
prefix tofn`foo, ${bar} and ${baz}`
fn
is called once withtemplate, ...expressions
template
is['foo, ', ' and ', '']
andexpressions
is[bar, baz]
- The result of
fn
becomes the value of the template literal - Possible use cases include input sanitization of expressions, parameter parsing, etc.
- Add a
- Template literals are almost strictly better than strings wrapped in single or double quotes
- Read ES6 Template Literals in Depth
Object Literals
- Instead of
{ foo: foo }
, you can just do{ foo }
-- known as a property value shorthand - Computed property names,
{ [prefix + 'Foo']: 'bar' }
, whereprefix: 'moz'
, yields{ mozFoo: 'bar' }
- You can't combine computed property names and property value shorthands,
{ [foo] }
is invalid - Method definitions in an object literal can be declared using an alternative, more terse syntax,
{ foo () {} }
- See also
Object
section - Read ES6 Object Literal Features in Depth
Classes
- Not "traditional" classes, syntax sugar on top of prototypal inheritance
- Syntax similar to declaring objects,
class Foo {}
- Instance methods --
new Foo().bar
-- are declared using the short object literal syntax,class Foo { bar () {} }
- Static methods --
Foo.isPonyFoo()
-- need astatic
keyword prefix,class Foo { static isPonyFoo () {} }
- Constructor method
class Foo { constructor () { /* initialize instance */ } }
- Prototypal inheritance with a simple syntax
class PonyFoo extends Foo {}
- Read ES6 Classes in Depth
Let and Const
let
andconst
are alternatives tovar
when declaring variableslet
is block-scoped instead of lexically scoped to afunction
let
is hoisted to the top of the block, whilevar
declarations are hoisted to top of the function- "Temporal Dead Zone" -- TDZ for short
- Starts at the beginning of the block where
let foo
was declared - Ends where the
let foo
statement was placed in user code (hoisiting is irrelevant here) - Attempts to access or assign to
foo
within the TDZ (before thelet foo
statement is reached) result in an error - Helps prevent mysterious bugs when a variable is manipulated before its declaration is reached
- Starts at the beginning of the block where
const
is also block-scoped, hoisted, and constrained by TDZ semanticsconst
variables must be declared using an initializer,const foo = 'bar'
- Assigning to
const
after initialization fails silently (or loudly -- with an exception -- under strict mode) const
variables don’t make the assigned value immutableconst foo = { bar: 'baz' }
meansfoo
will always reference the right-hand side objectconst foo = { bar: 'baz' }; foo.bar = 'boo'
won't throw
- Declaration of a variable by the same name will throw
- Meant to fix mistakes where you reassign a variable and lose a reference that was passed along somewhere else
- In ES6, functions are block scoped
- Prevents leaking block-scoped secrets through hoisting,
{ let _foo = 'secret', bar = () => _foo; }
- Doesn't break user code in most situations, and typically what you wanted anyways
- Prevents leaking block-scoped secrets through hoisting,
- Read ES6 Let, Const and the “Temporal Dead Zone” (TDZ) in Depth
Symbols
- A new primitive type in ES6
- You can create your own symbols using
var symbol = Symbol()
- You can add a description for debugging purposes, like
Symbol('ponyfoo')
- Symbols are immutable and unique.
Symbol()
,Symbol()
,Symbol('foo')
andSymbol('foo')
are all different - Symbols are of type
symbol
, thus:typeof Symbol() === 'symbol'
- You can also create global symbols with
Symbol.for(key)
- If a symbol with the provided
key
already existed, you get that one back - Otherwise, a new symbol is created, using
key
as its description as well Symbol.keyFor(symbol)
is the inverse function, taking asymbol
and returning itskey
- Global symbols are as global as it gets, or cross-realm. Single registry used to look up these symbols across the runtime
window
contexteval
context<iframe>
context,Symbol.for('foo') === iframe.contentWindow.Symbol.for('foo')
- If a symbol with the provided
- There's also "well-known" symbols
- Not on the global registry, accessible through
Symbol[name]
, e.g:Symbol.iterator
- Cross-realm, meaning
Symbol.iterator === iframe.contentWindow.Symbol.iterator
- Used by specification to define protocols, such as the iterable protocol over
Symbol.iterator
- They're not actually well-known -- in colloquial terms
- Not on the global registry, accessible through
- Iterating over symbol properties is hard, but not impossible and definitely not private
- Symbols are hidden to all pre-ES6 "reflection" methods
- Symbols are accessible through
Object.getOwnPropertySymbols
- You won't stumble upon them but you will find them if actively looking
- Read ES6 Symbols in Depth
Iterators
- Iterator and iterable protocol define how to iterate over any object, not just arrays and array-likes
- A well-known
Symbol
is used to assign an iterator to any object var foo = { [Symbol.iterator]: iterable}
, orfoo[Symbol.iterator] = iterable
- The
iterable
is a method that returns aniterator
object that has anext
method - The
next
method returns objects with two properties,value
anddone
- The
value
property indicates the current value in the sequence being iterated - The
done
property indicates whether there are any more items to iterate
- The
- Objects that have a
[Symbol.iterator]
value are iterable, because they subscribe to the iterable protocol - Some built-ins like
Array
,String
, orarguments
-- andNodeList
in browsers -- are iterable by default in ES6 - Iterable objects can be looped over with
for..of
, such asfor (let el of document.querySelectorAll('a'))
- Iterable objects can be synthesized using the spread operator, like
[...document.querySelectorAll('a')]
- You can also use
Array.from(document.querySelectorAll('a'))
to synthesize an iterable sequence into an array - Iterators are lazy, and those that produce an infinite sequence still can lead to valid programs
- Be careful not to attempt to synthesize an infinite sequence with
...
orArray.from
as that will cause an infinite loop - Read ES6 Iterators in Depth
Generators
- Generator functions are a special kind of iterator that can be declared using the
function* generator () {}
syntax - Generator functions use
yield
to emit an element sequence - Generator functions can also use
yield*
to delegate to another generator function -- or any iterable object - Generator functions return a generator object that's adheres to both the iterable and iterator protocols
- Given
g = generator()
,g
adheres to the iterable protocol becauseg[Symbol.iterator]
is a method - Given
g = generator()
,g
adheres to the iterator protocol becauseg.next
is a method - The iterator for a generator object
g
is the generator itself:g[Symbol.iterator]() === g
- Given
- Pull values using
Array.from(g)
,[...g]
,for (let item of g)
, or just callingg.next()
- Generator function execution is suspended, remembering the last position, in four different cases
- A
yield
expression returning the next value in the sequence - A
return
statement returning the last value in the sequence - A
throw
statement halts execution in the generator entirely - Reaching the end of the generator function signals
{ done: true }
- A
- Once the
g
sequence has ended,g.next()
simply returns{ done: true }
and has no effect - It's easy to make asynchronous flows feel synchronous
- Take user-provided generator function
- User code is suspended while asynchronous operations take place
- Call
g.next()
, unsuspending execution in user code
- Read ES6 Generators in Depth
Promises
- Follows the
Promises/A+
specification, was widely implemented in the wild before ES6 was standarized (e.gbluebird
) - Promises behave like a tree. Add branches with
p.then(handler)
andp.catch(handler)
- Create new
p
promises withnew Promise((resolve, reject) => { /* resolver */ })
- The
resolve(value)
callback will fulfill the promise with the providedvalue
- The
reject(reason)
callback will rejectp
with areason
error - You can call those methods asynchronously, blocking deeper branches of the promise tree
- The
- Each call to
p.then
andp.catch
creates another promise that's blocked onp
being settled - Promises start out in pending state and are settled when they're either fulfilled or rejected
- Promises can only be settled once, and then they're settled. Settled promises unblock deeper branches
- You can tack as many promises as you want onto as many branches as you need
- Each branch will execute either
.then
handlers or.catch
handlers, never both - A
.then
callback can transform the result of the previous branch by returning a value - A
.then
callback can block on another promise by returning it p.catch(fn).catch(fn)
won't do what you want -- unless what you wanted is to catch errors in the error handlerPromise.resolve(value)
creates a promise that's fulfilled with the providedvalue
Promise.reject(reason)
creates a promise that's rejected with the providedreason
Promise.all(...promises)
creates a promise that settles when all...promises
are fulfilled or 1 of them is rejectedPromise.race(...promises)
creates a promise that settles as soon as 1 of...promises
is settled- Use Promisees -- the promise visualization playground -- to better understand promises
- Read ES6 Promises in Depth
Maps
- A replacement to the common pattern of creating a hash-map using plain JavaScript objects
- Avoids security issues with user-provided keys
- Allows keys to be arbitrary values, you can even use DOM elements or functions as the
key
to an entry
Map
adheres to iterable protocol- Create a
map
usingnew Map()
- Initialize a map with an
iterable
like[[key1, value1], [key2, value2]]
innew Map(iterable)
- Use
map.set(key, value)
to add entries - Use
map.get(key)
to get an entry - Check for a
key
usingmap.has(key)
- Remove entries with
map.delete(key)
- Iterate over
map
withfor (let [key, value] of map)
, the spread operator,Array.from
, etc - Read ES6 Maps in Depth
WeakMaps
- Similar to
Map
, but not quite the same WeakMap
isn't iterable, so you don't get enumeration methods like.forEach
,.clear
, and others you had inMap
WeakMap
keys must be reference types. You can't use value types like symbols, numbers, or strings as keysWeakMap
entries with akey
that's the only reference to the referenced variable are subject to garbage collection- That last point means
WeakMap
is great at keeping around metadata for objects, while those objects are still in use - You avoid memory leaks, without manual reference counting -- think of
WeakMap
asIDisposable
in .NET - Read ES6 WeakMaps in Depth
Sets
- Similar to
Map
, but not quite the same Set
doesn't have keys, there's only valuesset.set(value)
doesn't look right, so we haveset.add(value)
instead- Sets can't have duplicate values because the values are also used as keys
- Read ES6 Sets in Depth
WeakSets
WeakSet
is sort of a cross-breed betweenSet
andWeakMap
- A
WeakSet
is a set that can't be iterated and doesn't have enumeration methods WeakSet
values must be reference typesWeakSet
may be useful for a metadata table indicating whether a reference is actively in use or not- Read ES6 WeakSets in Depth
Proxies
- Proxies are created with
new Proxy(target, handler)
, wheretarget
is any object andhandler
is configuration - The default behavior of a
proxy
acts as a passthrough to the underlyingtarget
object - Handlers determine how the underlying
target
object is accessed on top of regular object property access semantics - You pass off references to
proxy
and retain strict control over howtarget
can be interacted with - Handlers are also known as traps, these terms are used interchangeably
- You can create revocable proxies with
Proxy.revocable(target, handler)
- That method returns an object with
proxy
andrevoke
properties - You could destructure
var {proxy, revoke} = Proxy.revocable(target, handler)
for convenience - You can configure the
proxy
all the same as withnew Proxy(target, handler)
- After
revoke()
is called, theproxy
will throw on any operation, making it convenient when you can't trust consumers
- That method returns an object with
get
-- trapsproxy.prop
andproxy['prop']
set
-- trapsproxy.prop = value
andproxy['prop'] = value
has
-- trapsin
operatordeleteProperty
-- trapsdelete
operatordefineProperty
-- trapsObject.defineProperty
and declarative alternativesenumerate
-- trapsfor..in
loopsownKeys
-- trapsObject.keys
and related methodsapply
-- traps function callsconstruct
-- traps usage of thenew
operatorgetPrototypeOf
-- traps internal calls to[[GetPrototypeOf]]
setPrototypeOf
-- traps calls toObject.setPrototypeOf
isExtensible
-- traps calls toObject.isExtensible
preventExtensions
-- traps calls toObject.preventExtensions
getOwnPropertyDescriptor
-- traps calls toObject.getOwnPropertyDescriptor
- Read ES6 Proxies in Depth
- Read ES6 Proxy Traps in Depth
- Read More ES6 Proxy Traps in Depth
Reflection
Reflection
is a new static built-in (think ofMath
) in ES6Reflection
methods have sensible internals, e.gReflect.defineProperty
returns a boolean instead of throwing- There's a
Reflection
method for each proxy trap handler, and they represent the default behavior of each trap - Going forward, new reflection methods in the same vein as
Object.keys
will be placed in theReflection
namespace - Read ES6 Reflection in Depth
Number
- Use
0b
prefix for binary, and0o
prefix for octal integer literals Number.isNaN
andNumber.isFinite
are like their global namesakes, except that they don't coerce input toNumber
Number.parseInt
andNumber.parseFloat
are exactly the same as their global namesakesNumber.isInteger
checks if input is aNumber
value that doesn’t have a decimal partNumber.EPSILON
helps figure out negligible differences between two numbers -- e.g.0.1 + 0.2
and0.3
Number.MAX_SAFE_INTEGER
is the largest integer that can be safely and precisely represented in JavaScriptNumber.MIN_SAFE_INTEGER
is the smallest integer that can be safely and precisely represented in JavaScriptNumber.isSafeInteger
checks whether an integer is within those bounds, able to be represented safely and precisely- Read ES6
Number
Improvements in Depth
Math
Math.sign
-- sign function of a numberMath.trunc
-- integer part of a numberMath.cbrt
-- cubic root of value, or∛‾value
Math.expm1
--e
to thevalue
minus1
, orevalue - 1
Math.log1p
-- natural logarithm ofvalue + 1
, orln(value + 1)
Math.log10
-- base 10 logarithm ofvalue
, orlog10(value)
Math.log2
-- base 2 logarithm ofvalue
, orlog2(value)
Math.sinh
-- hyperbolic sine of a numberMath.cosh
-- hyperbolic cosine of a numberMath.tanh
-- hyperbolic tangent of a numberMath.asinh
-- hyperbolic arc-sine of a numberMath.acosh
-- hyperbolic arc-cosine of a numberMath.atanh
-- hyperbolic arc-tangent of a numberMath.hypot
-- square root of the sum of squaresMath.clz32
-- leading zero bits in the 32-bit representation of a numberMath.imul
-- C-like 32-bit multiplicationMath.fround
-- nearest single-precision float representation of a number- Read ES6
Math
Additions in Depth
Array
Array.from
-- createArray
instances from arraylike objects likearguments
or iterablesArray.of
-- similar tonew Array(...items)
, but without special casesArray.prototype.copyWithin
-- copies a sequence of array elements into somewhere else in the arrayArray.prototype.fill
-- fills all elements of an existing array with the provided valueArray.prototype.find
-- returns the first item to satisfy a callbackArray.prototype.findIndex
-- returns the index of the first item to satisfy a callbackArray.prototype.keys
-- returns an iterator that yields a sequence holding the keys for the arrayArray.prototype.values
-- returns an iterator that yields a sequence holding the values for the arrayArray.prototype.entries
-- returns an iterator that yields a sequence holding key value pairs for the arrayArray.prototype[Symbol.iterator]
-- exactly the same as theArray.prototype.values
method- Read ES6
Array
Extensions in Depth
Object
Object.assign
-- recursive shallow overwrite for properties fromtarget, ...objects
Object.is
-- like using the===
operator, buttrue
forNaN
vsNaN
, andfalse
for+0
vs-0
Object.getOwnPropertySymbols
-- returns all own property symbols found on an objectObject.setPrototypeOf
-- changes prototype. Equivalent toObject.prototype.__proto__
setter- See also Object Literals section
- Read ES6
Object
Changes in Depth
Strings and Unicode
- String Manipulation
String.prototype.startsWith
-- whether the string starts withvalue
String.prototype.endsWith
-- whether the string ends invalue
String.prototype.includes
-- whether the string containsvalue
anywhereString.prototype.repeat
-- returns the string repeatedamount
timesString.prototype[Symbol.iterator]
-- lets you iterate over a sequence of unicode code points (not characters)
- Unicode
String.prototype.codePointAt
-- base-10 numeric representation of a code point at a given position in stringString.fromCodePoint
-- given...codepoints
, returns a string made of their unicode representationsString.prototype.normalize
-- returns a normalized version of the string's unicode representation
- Read ES6 Strings and Unicode Additions in Depth
Modules
- Strict Mode is turned on by default in the ES6 module system
- ES6 modules are files that
export
an API export default value
exports a default bindingexport var foo = 'bar'
exports a named binding- Named exports are bindings that can be changed at any time from the module that's exporting them
export { foo, bar }
exports a list of named exportsexport { foo as ponyfoo }
aliases the export to be referenced asponyfoo
insteadexport { foo as default }
marks the named export as the default export- As a best practice,
export default api
at the end of all your modules, whereapi
is an object, avoids confusion - Module loading is implementation-specific, allows interoperation with CommonJS
import 'foo'
loads thefoo
module into the current moduleimport foo from 'ponyfoo'
assigns the default export ofponyfoo
to a localfoo
variableimport {foo, bar} from 'baz'
imports named exportsfoo
andbar
from thebaz
moduleimport {foo as bar} from 'baz'
imports named exportfoo
but aliased as abar
variableimport {default} from 'foo'
also imports the default exportimport {default as bar} from 'foo'
imports the default export aliased asbar
import foo, {bar, baz} from 'foo'
mixes defaultfoo
with named exportsbar
andbaz
in one declarationimport * as foo from 'foo'
imports the namespace object- Contains all named exports in
foo[name]
- Contains the default export in
foo.default
, if a default export was declared in the module
- Contains all named exports in
- Read ES6 Modules Additions in Depth
Time for a bullet point detox. Then again, I did warn you to read the article series instead. Don't forget to subscribe and maybe even contribute to keep Pony Foo alive. Also, did you try the Konami code just yet?