ES6 is the release that made JavaScript pleasant to write. This JavaScript ES6 tutorial covers every feature it introduced, and — more usefully — what each one replaced, because the ES6 syntax makes far more sense once you have seen the mess it was designed to fix.
What is ES6, exactly?
ES6 is ECMAScript 2015. Both names are correct and the confusion is TC39’s fault: the committee shipped ES5 in 2009, spent six years on the next release, and then switched to naming versions by year. So ES6 and ES2015 are the same thing, and everything after it is ES2016, ES2017 and so on.
In practice, when someone says “ES6” today they usually mean modern ES6 JavaScript generally, not the 2015 release specifically. Every browser still in use supports all of it.
let and const
Before ES6 there was only var, which had two problems: it ignored blocks, and it could be redeclared silently.
// Before — var leaks out of the block
for (var i = 0; i < 3; i++) { /* ... */ }
console.log(i); // 3 — still here
// After — let stays where it was declared
for (let j = 0; j < 3; j++) { /* ... */ }
console.log(j); // ReferenceErrorUse const by default and let only when the value is reassigned. var has no remaining use.
const does not mean immutable
This is the most common misunderstanding of the two. const stops the binding being reassigned; it does nothing to the value:
const user = { name: 'Asha' };
user.name = 'Ravi'; // fine — the object is not frozen
user = {}; // TypeError — the binding is
// If you want the object frozen too:
const frozen = Object.freeze({ name: 'Asha' });Arrow functions
// Before
var double = function (n) { return n * 2; };
// After
const double = n => n * 2;
const add = (a, b) => a + b;
// Returning an object needs parentheses, or the braces
// are read as a function body:
const make = name => ({ name });Arrow functions and this — the real reason they exist
Arrow functions are not just shorter. They do not have their own this; they use the one from where they were written. That single difference removed an entire category of bug:
// Before — this is wrong inside the callback
function Timer() {
this.seconds = 0;
var self = this; // the workaround everyone wrote
setInterval(function () {
self.seconds++;
}, 1000);
}
// After — this is the Timer, because the arrow did not rebind it
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++;
}, 1000);
}The flip side: never use an arrow function as an object method or a constructor. It will not have the this you expect.
Template literals
// Before
var greeting = 'Hello ' + name + ', you have ' + count + ' messages';
// After
const greeting = `Hello ${name}, you have ${count} messages`;
// And multi-line strings, which needed \n before:
const html = `
<div>
<h1>${title}</h1>
</div>
`;Destructuring
Pulling values out of objects and arrays, with defaults and renaming built in. Probably the ES6 feature you will use most often.
const response = { name: 'Asha', role: 'admin' };
// Basic
const { name, role } = response;
// With a default for a key that is missing
const { name, city = 'Delhi' } = response;
// Renaming, for when the API's names are not yours
const { name: userName } = response;
// Arrays, by position — the gap skips one
const [first, , third] = [1, 2, 3];
// Straight in a function signature
function greet({ name, role = 'guest' }) {
return `${name} (${role})`;
}One catch worth knowing: a default only applies when the value is undefined, not when it is null. An API returning null will sail past your default and give you null.
Spread and rest
Same three dots, opposite jobs. Spread pulls a thing apart; rest gathers things up.
// Spread — copying and merging
const copy = [ ...original ];
const joined = [ ...first, ...second ];
const merged = { ...defaults, ...overrides }; // later wins
// Rest — collecting the remainder
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
const { id, ...everythingElse } = user;A warning that costs people hours: spread makes a shallow copy. Nested objects are still shared with the original, so changing one changes both.
Default parameters
// Before
function connect(host, port) {
port = port || 5432; // breaks if you pass 0
// ...
}
// After
function connect(host, port = 5432) { /* ... */ }The old || trick treats 0, '' and false as missing. A real default parameter only fires on undefined.
Classes
Syntax over the prototype system that was already there. It does not add a new object model — it makes the existing one readable.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
static create(name) {
return new Animal(name);
}
}
class Dog extends Animal {
speak() {
return `${this.name} barks`;
}
}Modules: import and export
// maths.js
export const PI = 3.14159;
export function area(r) { return PI * r * r; }
export default function circumference(r) { return 2 * PI * r; }
// app.js
import circumference, { PI, area } from './maths.js';
import * as maths from './maths.js';One default per file, as many named exports as you like. In the browser this needs <script type="module">; in Node it needs "type": "module" in package.json or an .mjs extension.
Promises
The replacement for nested callbacks. Worth learning properly, because async/await — which came later, in ES2017 — is built directly on them.
const request = new Promise((resolve, reject) => {
if (ok) { resolve(data); } else { reject(new Error('failed')); }
});
request
.then(data => process(data))
.catch(error => console.error(error))
.finally(() => stopSpinner());
// Several at once
const [users, posts] = await Promise.all([ getUsers(), getPosts() ]);Map and Set
// Set — unique values
const seen = new Set([1, 2, 2, 3]); // {1, 2, 3}
const unique = [ ...new Set(list) ]; // the one-line dedupe
// Map — keys can be anything, and order is kept
const byId = new Map();
byId.set(userObject, 'metadata'); // an object as a key
byId.get(userObject);Use a Map over a plain object when keys are not strings, when insertion order matters, or when you need .size without counting.
for…of
const list = ['a', 'b', 'c'];
for (const item of list) { } // values — usually what you want
for (const key in list) { } // keys, and inherited ones too
list.forEach(item => { }); // no break, no awaitfor...of is the one that supports break, continue and await inside the loop. forEach supports none of them.
Symbols and generators
The two ES6 features you can safely learn last. Symbols are unique keys that will not collide with anyone else’s; generators are functions that pause and resume.
const id = Symbol('id');
user[id] = 123; // invisible to Object.keys
function* counter() {
let i = 0;
while (i < 3) { yield i++; }
}
for (const n of counter()) { } // 0, 1, 2You will meet both more often through libraries than in code you write yourself.
What came after ES6
ES6 is the large one, but the yearly releases since have added things you will use just as much:
- ES2016 — the exponent operator
**andArray.includes() - ES2017 —
async/await, which most people think of as ES6 and is not - ES2018 — object spread, which is also often assumed to be ES6
- ES2019 —
Array.flat(),Object.fromEntries() - ES2020 — optional chaining
?.and nullish coalescing?? - ES2021 and later —
replaceAll(), logical assignment, top-level await
Optional chaining and nullish coalescing in particular are worth adding to your habits straight after ES6. They remove more code than most of ES6 did.
Common questions
Is ES6 the same as ES2015?
Yes, exactly the same release. ES6 was the working name; ES2015 is the published one. Both are used interchangeably.
Do I still need Babel for ES6?
No. Every browser in current use supports all of ES6 natively. Babel is still worth having for features newer than about ES2022, or if you must support a browser that stopped updating years ago.
In what order should I learn ES6?
Start with four ES6 concepts: const and let, arrow functions, template literals, and destructuring. Those appear in nearly every line of modern JavaScript. Classes, modules and promises follow. Symbols and generators can wait.
Is var completely dead?
For new code, yes. You will still meet it in older codebases, and it is worth understanding why it behaves as it does — but there is no situation where var is the better choice today.
Views: 629