Key Takeaways
- ES6 means ECMAScript 2015, a specific edition of the JavaScript language standard—not all modern JavaScript.
- Start with block-scoped declarations, template literals, default parameters, destructuring, and rest/spread syntax.
- Arrow functions capture the surrounding
this; they aren't interchangeable with ordinary functions. - Modules, classes, Promises, and collections remain foundational, but compatibility depends on your actual runtime targets.
The most useful JavaScript ES6 features include let and const, template literals, arrow functions, destructuring, modules, classes, and Promises. ES6 is another name for ECMAScript 2015; these features remain foundational even though they are no longer new.
Learn what each feature changes—not just how to shorten your code. The examples below preserve the distinction between ES2015 and later additions, and highlight the behavior that tends to surprise beginners.
ECMAScript: A Brief History
JavaScript appeared in 1995. ECMAScript is the specification that standardizes its core language; JavaScript didn't simply change its name to ECMAScript. The first edition of ECMA-262 was published in June 1997, followed by ES3 in December 1999, ES5 in December 2009, and ES2015 in June 2015.
After ES2015, ECMAScript moved to annual editions. That matters when reading tutorials: async/await belongs to ES2017, while object rest/spread belongs to ES2018. Neither is an ES6 feature.
What's New in ES6?
Think of ES2015 in three groups: everyday syntax for variables and functions, structure for larger programs, and built-in capabilities for asynchronous work and collections. This is a practical shortlist, not an exhaustive inventory of the edition.
| Feature group | Edition | Examples |
|---|---|---|
| Everyday syntax | ES2015 (ES6) | Block-scoped declarations, template literals, arrow functions, defaults, destructuring, array spread, rest parameters |
| Program structure and built-ins | ES2015 (ES6) | Classes, modules, Promises, Map, Set, for...of |
| Later asynchronous syntax | ES2017 | async/await |
| Later object syntax | ES2018 | Object rest/spread |
The ES2015 specification and later edition introductions document these boundaries.
Block-Scoped Declarations: let and const
A block is a region enclosed by braces. Both let and const are block-scoped; var is not. Use const when you won't reassign a binding—the association between a variable name and its value—and let when you will.
{
var legacy = "still visible";
let count = 0;
const MAX = 100;
count += 1;
console.log(count, MAX); // 1 100
}
console.log(legacy); // "still visible"
// console.log(count); // ReferenceError: outside its blockconst prevents reassignment, not mutation of an object or array. These are different operations:
const settings = { retries: 1 };
settings.retries = 2; // Allowed: mutate the object
// settings = { retries: 3 }; // TypeError: reassign the bindingBoth declarations also have a temporal dead zone: accessing the binding before its initialization throws a ReferenceError. Declare variables before using them rather than relying on hoisting.
For more scope examples, see our comparison of JavaScript let, const, and var.
Template Literals
Template literals use backticks. They support embedded expressions with ${expression} and literal line breaks, so you don't need concatenation for simple formatted strings.
const name = "World";
const message = `Hello,
${name}!`;
console.log(message);
// Hello,
// World!Use them when interpolation makes the result easier to read. Our ES6 strings tutorial explores related string operations.
Default Function Parameters
A default parameter applies when an argument is omitted or explicitly undefined. It doesn't replace null, 0, false, or an empty string.
function calculate(x, y = 4, z = 30) {
return x + y + z;
}
console.log(calculate(2)); // 36
console.log(calculate(2, undefined)); // 36
console.log(calculate(2, null)); // 32: null doesn't trigger the defaultA default is a fallback for a missing value, not input validation. If a function requires a number, validate that requirement separately.
Arrow Functions and Lexical this
Arrow functions offer compact function expressions. An expression body returns its result implicitly:
const add = (x, y) => x + y;
console.log(add(2, 3)); // 5The important difference is lexical this: an arrow uses this from its surrounding scope instead of receiving a new value when called. Compare these callbacks:
const counter = {
count: 2,
compare() {
const regular = function () { return this.count; };
const arrow = () => this.count;
console.log(regular.call({ count: 10 })); // 10
console.log(arrow.call({ count: 10 })); // 2
}
};
counter.compare();Arrows have no own arguments object and cannot be constructors. Don't use an arrow as an object method when the method needs this to refer to its receiver; use method syntax, as compare() does above.
Event handlers need the same care. An ordinary function registered with addEventListener() receives the listener's current target as this; an arrow doesn't. With an arrow, read event.currentTarget explicitly when that's what you need.
See our JavaScript functions tutorial for more function patterns.
Destructuring Assignment
Destructuring unpacks object properties or iterable values into variables. Object patterns select properties by name; array patterns select values by position. You can also rename an extracted property or supply a default.
const obj = { x: 1, y: 2 };
const { x, y } = obj;
console.log(x, y); // 1 2
const { x: horizontal, z = 0 } = obj;
const [first, second] = ["a", "b"];
console.log(horizontal, z, first, second); // 1 0 "a" "b"A destructuring default applies to a missing or undefined value, not null. Destructuring null or undefined itself throws a TypeError, so validate uncertain inputs before unpacking them.
Rest Parameters and Spread Syntax
Rest gathers remaining function arguments into an array. Spread expands an iterable into function arguments or array elements. They share the ... notation but work in opposite directions.
function sum(...values) {
let total = 0;
for (const value of values) {
total += value;
}
return total;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6
console.log([0, ...numbers, 4]); // [0, 1, 2, 3, 4]A rest parameter must be last. Array spread creates a shallow copy: nested objects are still shared, not recursively cloned.
Class Syntax Still Uses Prototypes
Classes organize object initialization and shared methods; they don't replace JavaScript's prototype-based inheritance model. A prototype is another object that JavaScript consults when a property isn't found directly on an object.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise`);
}
}
class Dog extends Animal {
speak() {
super.speak();
console.log(`${this.name} barks`);
}
}
const dog = new Dog("Rex");
dog.speak();
// Rex makes a noise
// Rex barksextends establishes inheritance, and super.speak() calls the parent implementation. Here, instance methods live on the class prototypes:
dog --> Dog.prototype --> Animal.prototype --> Object.prototype --> null
speak() speak()If you write a derived constructor that uses this, call super(...) before accessing it. Our introduction to ES6 classes develops this model further.
ES2015 Modules: import and export
Modules declare which values a file exports and which dependencies it imports. Module syntax, runtime loading, and bundling are separate concerns: you don't need a bundler simply to use modules.
With Node.js installed, save these two files in the same directory. The .mjs extension explicitly identifies an ECMAScript module.
// math.mjs
export function add(x, y) {
return x + y;
}// app.mjs
import { add } from "./math.mjs";
console.log(add(2, 3)); // 5node app.mjsKeep the file extension in the relative import; Node.js requires it for this form of module resolution. See the Node.js ECMAScript modules documentation for the loading rules.
Browsers can load modules natively when the entry script uses type="module". Serve browser examples through a local web server rather than opening them with a file:// URL.
Promises for Asynchronous Programming
A Promise represents an eventual result: it starts pending and can become fulfilled with a value or rejected with a reason. Chaining lets you transform results and handle failures without deeply nesting callbacks.
const promise = new Promise(resolve => {
setTimeout(() => resolve("done"), 100);
});
promise
.then(message => `Result: ${message}`)
.then(message => console.log(message)) // "Result: done"
.catch(error => console.error("Operation failed:", error));This example settles instead of leaving a Promise pending forever. The timer requests a 100-millisecond delay; actual execution can be later. setTimeout() is a runtime API, not an ECMAScript language feature.
then() returns a new Promise, enabling the chain; catch() handles rejections that reach it. Return asynchronous work from a handler when the next step must wait for it.
Promises are ES2015. The later async/await syntax, introduced in ES2017, works with Promises rather than replacing them. Continue with our JavaScript Promises tutorial.
Other Notable ES2015 Features
Map, Set, and for...of
Map stores key/value entries and accepts keys of any type. Set stores unique values. Both iterate in insertion order, making them useful for lookups and deduplication without inventing your own collection conventions.
const roles = new Map();
roles.set("Ada", "maintainer");
roles.set("Lin", "reviewer");
console.log(roles.get("Ada")); // "maintainer"
for (const [name, role] of roles) {
console.log(`${name}: ${role}`);
}
const languages = new Set(["JavaScript", "JavaScript", "CSS"]);
console.log([...languages]); // ["JavaScript", "CSS"]An iterable supplies values that iteration constructs can consume. for...of works with arrays, strings, Maps, Sets, and other iterables; for...in instead enumerates string-keyed enumerable properties. Also, Sets compare objects by identity, not by matching their contents.
String, Array, and Math Utilities
ES2015 also added practical built-ins. String includes() searches for a substring, startsWith() checks a prefix, Array.from() creates arrays from iterable or array-like inputs, and find() returns the first matching element or undefined.
console.log("ES2015".includes("2015")); // true
console.log("report.csv".startsWith("report")); // true
console.log(Array.from("abc")); // ["a", "b", "c"]
console.log([2, 7, 9].find(value => value > 5)); // 7
console.log(Math.trunc(-4.9)); // -4
console.log(Number.isInteger(4)); // trueMath.trunc() removes the fractional part rather than rounding down, and Number.isInteger() tests whether a value is an integer number.
Browser Support and Compatibility Tooling
The ES2015 features shown here are broadly supported in current evergreen browsers. That isn't a guarantee for every older browser, embedded engine, or runtime: check the features against the environments your project actually supports.
Keep three separate jobs in mind:
- Syntax transformation: a compiler such as Babel can rewrite configured syntax for your targets. Babel's
@babel/preset-envselects transformations using compatibility data. - Runtime APIs: a polyfill supplies a missing built-in implementation. Rewriting arrow functions doesn't, by itself, provide
PromiseorMap. Polyfills need their own configuration. - Bundling: webpack builds a dependency graph and produces bundles. Bundling alone doesn't guarantee that the output's syntax and APIs work in your target environments.
A framework's APIs, your application's JavaScript syntax, and the build pipeline's runtime targets are separate compatibility questions. Don't treat “ES6 compliant” as a useful blanket certification for a framework.
For production, declare your supported browser and runtime targets, configure only the transformations and polyfills they require, and test the built output in those environments. Supporting legacy browsers should be an explicit requirement, not an assumption that everything must become ES5.
FAQ
What does ES6 mean in JavaScript?
ES6 means ECMAScript 2015, the sixth edition of the specification that standardizes JavaScript's core language. It is not a synonym for every feature in modern JavaScript.
Which features were introduced in ES2015?
ES2015 introduced let and const, template literals, default parameters, arrow functions, destructuring, rest parameters, array spread, classes, modules, Promises, Map, Set, and for...of, among other features.
Is async/await part of ES6?
No. Promises were introduced in ES2015, while async/await arrived in ES2017 as syntax for working with Promise-based operations.
Are ES6 features supported by modern browsers?
The ES2015 features covered here are broadly supported in current evergreen browsers. Check individual features against your supported browser and runtime versions rather than relying on a blanket ES6 support claim.
Do I still need Babel to use ES6?
Not necessarily: environments that support your chosen features can run them directly. Babel is useful when your targets require syntax transformations; missing runtime APIs require separately configured polyfills.
Further Reading
- ECMAScript 2015 language specification — the definitive scope and semantics of ES6.
- MDN: const — block scope, reassignment, and mutable objects.
- MDN: Arrow function expressions — lexical this and important limitations.
- MDN: Using classes — constructors, prototypes, and inheritance.
- MDN: JavaScript modules — imports, exports, and browser loading.
- MDN: Promise — states, chaining, and error handling.
- MDN: Keyed collections — practical Map and Set operations.
- Babel: @babel/preset-env — target-driven transformations and compatibility configuration.
