JavaScript ES2018

Rest/spread for objects and async iteration

🔄 What is ES2018?

ES2018 brought rest/spread properties for objects, async iteration, and improved regular expressions. These features made object manipulation and asynchronous programming even more powerful.


// ES2018 introduced object rest/spread
const person = { name: "John", age: 30, city: "NYC" };
const { name, ...rest } = person;
console.log(name); // "John"
console.log(rest); // { age: 30, city: "NYC" }

// Object spread
const newPerson = { ...person, job: "Developer" };
console.log(newPerson);
                                    

Output:

John
{ age: 30, city: "NYC" }
{ name: "John", age: 30, city: "NYC", job: "Developer" }

ES2018 Features

📦

Object Rest/Spread

Rest and spread for objects

const {a, ...rest} = obj;
const newObj = {...obj, b: 2};
🔄

Async Iteration

for-await-of loops

for await (const item of asyncIterable) {
  console.log(item);
}
🎯

Promise.finally()

Always execute code after promise

promise
  .then(result => {})
  .finally(() => cleanup());
🔍

RegExp Improvements

Named capture groups & more

const regex = /(?\d{4})/;
const match = regex.exec("2023");

🔹 Object Rest and Spread

Extract and combine object properties easily:

// Object Rest - extract remaining properties
const user = {
    id: 1,
    name: "Alice",
    email: "[email protected]",
    age: 28,
    city: "Boston"
};

// Extract specific properties, rest goes into 'others'
const { id, name, ...others } = user;
console.log(id);     // 1
console.log(name);   // "Alice"
console.log(others); // { email: "[email protected]", age: 28, city: "Boston" }

// Object Spread - combine objects
const basicInfo = { name: "Bob", age: 25 };
const contactInfo = { email: "[email protected]", phone: "123-456-7890" };
const address = { city: "Seattle", state: "WA" };

// Combine all objects
const completeProfile = {
    ...basicInfo,
    ...contactInfo,
    ...address,
    status: "active"
};

console.log(completeProfile);

// Override properties
const originalSettings = { theme: "light", fontSize: 14, autoSave: true };
const userPreferences = { theme: "dark", fontSize: 16 };

const finalSettings = { ...originalSettings, ...userPreferences };
console.log(finalSettings); // theme and fontSize are overridden

Output:

1
Alice
{ email: "[email protected]", age: 28, city: "Boston" }
{ name: "Bob", age: 25, email: "[email protected]", phone: "123-456-7890", city: "Seattle", state: "WA", status: "active" }
{ theme: "dark", fontSize: 16, autoSave: true }

🔹 Async Iteration (for-await-of)

Iterate over async data sources:

// Create an async generator
async function* asyncGenerator() {
    yield await Promise.resolve(1);
    yield await Promise.resolve(2);
    yield await Promise.resolve(3);
}

// Use for-await-of to iterate
async function processAsyncData() {
    console.log("Starting async iteration...");
    
    for await (const value of asyncGenerator()) {
        console.log("Received:", value);
    }
    
    console.log("Async iteration complete!");
}

processAsyncData();

// Real-world example: Processing API responses
async function* fetchUserData(userIds) {
    for (const id of userIds) {
        // Simulate API call
        const response = await new Promise(resolve => 
            setTimeout(() => resolve({ id, name: `User${id}` }), 100)
        );
        yield response;
    }
}

async function displayUsers() {
    const userIds = [1, 2, 3, 4, 5];
    
    console.log("Fetching users...");
    for await (const user of fetchUserData(userIds)) {
        console.log(`Loaded: ${user.name} (ID: ${user.id})`);
    }
    console.log("All users loaded!");
}

displayUsers();

Output:

Starting async iteration...
Received: 1
Received: 2
Received: 3
Async iteration complete!
Fetching users...
Loaded: User1 (ID: 1)
Loaded: User2 (ID: 2)
Loaded: User3 (ID: 3)
Loaded: User4 (ID: 4)
Loaded: User5 (ID: 5)
All users loaded!

🔹 Promise.finally()

Execute code regardless of promise outcome:

// Promise.finally() always runs
function fetchData(shouldSucceed) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (shouldSucceed) {
                resolve("Data loaded successfully!");
            } else {
                reject(new Error("Failed to load data"));
            }
        }, 1000);
    });
}

// Example 1: Successful promise
fetchData(true)
    .then(result => {
        console.log("Success:", result);
    })
    .catch(error => {
        console.log("Error:", error.message);
    })
    .finally(() => {
        console.log("Cleanup: Hiding loading spinner");
    });

// Example 2: Failed promise
fetchData(false)
    .then(result => {
        console.log("Success:", result);
    })
    .catch(error => {
        console.log("Error:", error.message);
    })
    .finally(() => {
        console.log("Cleanup: Hiding loading spinner");
    });

// Practical example: File upload with loading state
async function uploadFile(file) {
    const loadingElement = document.getElementById('loading');
    
    try {
        // Show loading
        if (loadingElement) loadingElement.style.display = 'block';
        
        // Simulate file upload
        const result = await new Promise((resolve, reject) => {
            setTimeout(() => {
                Math.random() > 0.5 ? resolve("Upload successful!") : reject(new Error("Upload failed"));
            }, 2000);
        });
        
        console.log(result);
        return result;
        
    } catch (error) {
        console.error("Upload error:", error.message);
        throw error;
        
    } finally {
        // Always hide loading, regardless of success or failure
        if (loadingElement) loadingElement.style.display = 'none';
        console.log("Loading state cleared");
    }
}

// Usage
uploadFile("document.pdf");

Output:

Success: Data loaded successfully!
Cleanup: Hiding loading spinner
Error: Failed to load data
Cleanup: Hiding loading spinner
Loading state cleared
(Upload result varies)

🔹 RegExp Named Capture Groups

Give names to regex capture groups for better readability:

// Old way with numbered groups
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
const dateMatch = dateRegex.exec("2023-12-25");
console.log("Year:", dateMatch[1]);  // Hard to remember which is which
console.log("Month:", dateMatch[2]);
console.log("Day:", dateMatch[3]);

// New way with named groups
const namedDateRegex = /(?\d{4})-(?\d{2})-(?\d{2})/;
const namedMatch = namedDateRegex.exec("2023-12-25");
console.log("Year:", namedMatch.groups.year);   // Much clearer!
console.log("Month:", namedMatch.groups.month);
console.log("Day:", namedMatch.groups.day);

// Email validation with named groups
const emailRegex = /(?[a-zA-Z0-9._%+-]+)@(?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/;
const emailMatch = emailRegex.exec("[email protected]");

if (emailMatch) {
    console.log("Username:", emailMatch.groups.username);
    console.log("Domain:", emailMatch.groups.domain);
}

// Phone number parsing
const phoneRegex = /(?\d{3})-(?\d{3})-(?\d{4})/;
const phoneMatch = phoneRegex.exec("555-123-4567");

if (phoneMatch) {
    const { area, exchange, number } = phoneMatch.groups;
    console.log(`Area: ${area}, Exchange: ${exchange}, Number: ${number}`);
}

// URL parsing
const urlRegex = /(?https?):\/\/(?[^\/]+)(?\/.*)?/;
const urlMatch = urlRegex.exec("https://www.example.com/path/to/page");

if (urlMatch) {
    console.log("Protocol:", urlMatch.groups.protocol);
    console.log("Domain:", urlMatch.groups.domain);
    console.log("Path:", urlMatch.groups.path || "/");
}

Output:

Year: 2023
Month: 12
Day: 25
Year: 2023
Month: 12
Day: 25
Username: john.doe
Domain: example.com
Area: 555, Exchange: 123, Number: 4567
Protocol: https
Domain: www.example.com
Path: /path/to/page

🧠 Test Your Knowledge

Which ES2018 feature allows you to extract remaining object properties?