ES6 (ECMAScript 2015) revolutionized JavaScript, introducing powerful features that transformed how we write code. From let and const for better variable scoping, to arrow functions for concise syntax, to classes for object-oriented programming, ES6 and subsequent versions (ES2016, ES2017, etc.) have modernized JavaScript dramatically. This lesson provides a comprehensive overview of the most important modern JavaScript features you'll use daily. Let's explore what makes modern JavaScript so powerful!
What is ES6+?
ECMAScript Versions:
- ES5 (2009): Strict mode, JSON support, array methods
- ES6/ES2015 (2015): Major update - let/const, arrows, classes, promises
- ES2016+: Yearly releases with incremental improvements
- Modern JavaScript: Refers to ES6 and beyond
ES6 was the biggest update to JavaScript since its creation. It introduced so many features that "ES6" became synonymous with "modern JavaScript."
let and const (Block Scope)
Why let and const?
// Old way: var (function scope, hoisting issues)
function oldWay() {
if (true) {
var x = 5;
}
console.log(x); // 5 - accessible outside block!
}
// Modern way: let (block scope)
function modernWay() {
if (true) {
let x = 5;
console.log(x); // 5 - works inside block
}
// console.log(x); // ReferenceError - not accessible outside!
}
// var hoisting problem
console.log(a); // undefined (hoisted)
var a = 5;
// let/const don't hoist this way
// console.log(b); // ReferenceError (temporal dead zone)
let b = 5;let vs const
// let - can be reassigned
let count = 0;
count = 1; // ✓ OK
count++; // ✓ OK
// const - cannot be reassigned
const MAX = 100;
// MAX = 200; // ✗ Error - cannot reassign
// const with objects - object itself can't change, but properties can
const user = { name: 'Alice' };
user.name = 'Bob'; // ✓ OK - modifying property
user.age = 25; // ✓ OK - adding property
// user = {}; // ✗ Error - cannot reassign
// const with arrays
const numbers = [1, 2, 3];
numbers.push(4); // ✓ OK - modifying array
numbers[0] = 99; // ✓ OK - changing element
// numbers = []; // ✗ Error - cannot reassign
// Best practice: Use const by default, let only when needed
const pi = 3.14159;
let counter = 0;
counter++;let/const are more predictable
var (Old)
// Old: var (function scope)
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 3, 3, 3
}, 100);
}
// i is 3 for all callbacks!
console.log(i); // 3 - leaked!
// var allows redeclaration
var name = 'Alice';
var name = 'Bob'; // No error!
// Hoisting confusion
console.log(x); // undefined
var x = 5;let/const (Modern)
// Modern: let (block scope)
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2
}, 100);
}
// Each iteration has own i!
// console.log(i); // Error - not leaked
// let prevents redeclaration
let name = 'Alice';
// let name = 'Bob'; // Error!
// No confusing hoisting
// console.log(x); // ReferenceError
let x = 5;Arrow Functions
// Traditional function
function add(a, b) {
return a + b;
}
// Arrow function - concise!
const add = (a, b) => {
return a + b;
};
// Even shorter (implicit return)
const add = (a, b) => a + b;
// Single parameter - no parentheses needed
const square = x => x * x;
// No parameters - empty parentheses
const greet = () => console.log('Hello');
// Multiple lines - explicit return
const calculate = (a, b) => {
let sum = a + b;
let product = a * b;
return { sum, product };
};
// Returning objects - wrap in parentheses
const makePerson = (name, age) => ({ name, age });
console.log(add(5, 3)); // 8
console.log(square(4)); // 16
console.log(makePerson('Alice', 25)); // { name: 'Alice', age: 25 }Lexical 'this' Binding
// Traditional function - 'this' is dynamic
const obj1 = {
name: 'Object 1',
sayHi: function() {
setTimeout(function() {
console.log('Hi from', this.name); // undefined
// 'this' is window/global, not obj1
}, 100);
}
};
// Arrow function - 'this' is lexical (inherited)
const obj2 = {
name: 'Object 2',
sayHi: function() {
setTimeout(() => {
console.log('Hi from', this.name); // "Object 2"
// 'this' is obj2, inherited from sayHi
}, 100);
}
};
// Practical example: Array methods
class Counter {
constructor() {
this.count = 0;
}
// Traditional function - need .bind(this)
startOld() {
setInterval(function() {
this.count++; // Error! 'this' is not Counter
console.log(this.count);
}.bind(this), 1000); // Need bind!
}
// Arrow function - 'this' works automatically
startNew() {
setInterval(() => {
this.count++; // Works! 'this' is Counter
console.log(this.count);
}, 1000);
}
}Template Literals
// Old way: String concatenation
let name = 'Alice';
let age = 25;
let message = 'Hello, my name is ' + name + ' and I am ' + age + ' years old.';
// Modern way: Template literals
let message = `Hello, my name is ${name} and I am ${age} years old.`;
// Expressions inside ${}
let a = 5;
let b = 10;
console.log(`${a} + ${b} = ${a + b}`); // "5 + 10 = 15"
// Multi-line strings (no need for \n)
let html = `
<div>
<h1>Title</h1>
<p>Paragraph</p>
</div>
`;
// Function calls
function greet(name) {
return `Hello, ${name}!`;
}
console.log(`Message: ${greet('Bob')}`);
// Object properties
let user = { name: 'Alice', age: 25 };
console.log(`User: ${user.name}, Age: ${user.age}`);
// Conditional expressions
let status = 'online';
console.log(`User is ${status === 'online' ? 'available' : 'away'}`);Destructuring
Array Destructuring
// Old way
let arr = [1, 2, 3];
let first = arr[0];
let second = arr[1];
let third = arr[2];
// Modern way: Destructuring
let [first, second, third] = [1, 2, 3];
console.log(first, second, third); // 1 2 3
// Skip elements
let [a, , c] = [1, 2, 3];
console.log(a, c); // 1 3
// Rest operator
let [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest); // [2, 3, 4, 5]
// Default values
let [x, y, z = 0] = [1, 2];
console.log(z); // 0 (default)
// Swapping variables
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1Object Destructuring
// Old way
let user = { name: 'Alice', age: 25, city: 'NYC' };
let name = user.name;
let age = user.age;
// Modern way: Destructuring
let { name, age, city } = user;
console.log(name, age, city); // Alice 25 NYC
// Different variable names
let { name: userName, age: userAge } = user;
console.log(userName, userAge); // Alice 25
// Default values
let { name, country = 'USA' } = user;
console.log(country); // USA (default)
// Nested destructuring
let person = {
name: 'Bob',
address: {
city: 'NYC',
zip: '10001'
}
};
let { name, address: { city, zip } } = person;
console.log(name, city, zip); // Bob NYC 10001
// Function parameters
function greet({ name, age }) {
console.log(`Hello ${name}, age ${age}`);
}
greet({ name: 'Alice', age: 25 });Default Parameters
// Old way: Manual defaults
function greet(name, message) {
name = name || 'Guest';
message = message || 'Hello';
console.log(message + ', ' + name);
}
// Modern way: Default parameters
function greet(name = 'Guest', message = 'Hello') {
console.log(`${message}, ${name}`);
}
greet(); // "Hello, Guest"
greet('Alice'); // "Hello, Alice"
greet('Bob', 'Hi'); // "Hi, Bob"
// Defaults can use other parameters
function createUser(name, age = 18, active = true) {
return { name, age, active };
}
// Defaults can be expressions
function calculate(a, b = a * 2) {
return a + b;
}
console.log(calculate(5)); // 15 (5 + 10)
console.log(calculate(5, 3)); // 8 (5 + 3)
// Defaults can call functions
function getDefaultName() {
return 'Anonymous';
}
function register(name = getDefaultName()) {
console.log('Registering:', name);
}Enhanced Object Literals
let name = 'Alice';
let age = 25;
// Old way
let user = {
name: name,
age: age,
greet: function() {
console.log('Hello');
}
};
// Modern way: Shorthand
let user = {
name, // Same as name: name
age, // Same as age: age
greet() { // Method shorthand
console.log('Hello');
}
};
// Computed property names
let prop = 'color';
let obj = {
[prop]: 'blue', // Dynamic property name
['user_' + 123]: 'Alice' // Computed name
};
console.log(obj.color); // "blue"
console.log(obj.user_123); // "Alice"
// Method shorthand
let calculator = {
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
}
};
console.log(calculator.add(5, 3)); // 8Classes
// Old way: Constructor functions
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hi, I'm ${this.name}`);
};
// Modern way: Classes
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hi, I'm ${this.name}`);
}
// Getters
get info() {
return `${this.name}, ${this.age} years old`;
}
// Static methods
static species() {
return 'Homo sapiens';
}
}
let alice = new Person('Alice', 25);
alice.greet(); // "Hi, I'm Alice"
console.log(alice.info); // "Alice, 25 years old"
console.log(Person.species()); // "Homo sapiens"
// Inheritance
class Employee extends Person {
constructor(name, age, title) {
super(name, age); // Call parent constructor
this.title = title;
}
greet() {
super.greet(); // Call parent method
console.log(`I'm a ${this.title}`);
}
}
let bob = new Employee('Bob', 30, 'Developer');
bob.greet();
// "Hi, I'm Bob"
// "I'm a Developer"Promises and Async/Await (Recap)
// ES6: Promises
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data loaded');
}, 1000);
});
}
fetchData()
.then(data => console.log(data))
.catch(error => console.log(error));
// ES2017: Async/Await
async function loadData() {
try {
let data = await fetchData();
console.log(data);
} catch (error) {
console.log(error);
}
}
loadData();Other Modern Features
for...of Loop
// Old way
let arr = [1, 2, 3, 4, 5];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// Modern way: for...of
for (let value of arr) {
console.log(value); // 1, 2, 3, 4, 5
}
// Works with strings
for (let char of 'hello') {
console.log(char); // h, e, l, l, o
}
// With array methods
let numbers = [1, 2, 3, 4, 5];
for (let num of numbers) {
if (num > 3) console.log(num); // 4, 5
}Map and Set
// Map - key-value pairs (any type as key)
let map = new Map();
map.set('name', 'Alice');
map.set(1, 'one');
map.set(true, 'yes');
console.log(map.get('name')); // "Alice"
console.log(map.size); // 3
console.log(map.has('name')); // true
// Set - unique values
let set = new Set([1, 2, 3, 3, 4, 4, 5]);
console.log(set); // Set { 1, 2, 3, 4, 5 }
console.log(set.size); // 5
set.add(6);
set.delete(1);
console.log(set.has(3)); // true
// Remove duplicates from array
let arr = [1, 2, 2, 3, 3, 4];
let unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3, 4]Array Methods
// find and findIndex
let users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
let user = users.find(u => u.id === 2);
console.log(user); // { id: 2, name: 'Bob' }
let index = users.findIndex(u => u.name === 'Charlie');
console.log(index); // 2
// includes (ES2016)
let arr = [1, 2, 3, 4, 5];
console.log(arr.includes(3)); // true
console.log(arr.includes(6)); // falseModern JavaScript Features
Explore ES6+ enhancements
console.log() to see your output in the console above.Key Takeaways
letandconstprovide block scope (use const by default)- Arrow functions: concise syntax and lexical 'this'
- Template literals: backticks with ${} for embedded expressions
- Destructuring: extract values from arrays/objects easily
- Default parameters: function parameters with default values
- Enhanced objects: shorthand properties and methods
- Classes: cleaner syntax for object-oriented programming
- Promises and async/await: modern async handling
- for...of: iterate over arrays, strings, and iterables
- Map and Set: new collection types
What's Next?
You now have a comprehensive overview of modern JavaScript features from ES6 onwards! These features make JavaScript more powerful, expressive, and easier to write. You've seen the evolution from old patterns to modern best practices.
In the next lesson, we'll dive deep into Modules (import/export)—learning how to organize code into reusable modules, export functionality, and import what you need. Modules are essential for building maintainable applications!
💪 Practice Challenge:
Before moving on, try:
- Replace all var with let/const in existing code
- Convert regular functions to arrow functions where appropriate
- Use template literals instead of string concatenation
- Apply destructuring to simplify variable extraction
- Create a class with inheritance using extends