Skip to main content

Course Progress

Loading...

ES6+ Features Overview

Duration: 45 minutes
Module 1: Modern JavaScript & jQuery

Learning Objectives

  • Master the concepts in this lesson
  • Apply knowledge through practice
  • Build practical skills
  • Prepare for next topics

Introduction to Modern JavaScript

Welcome to our exploration of ES6+ features! JavaScript has evolved dramatically since the release of ECMAScript 2015 (ES6), which introduced numerous powerful features that transformed how we write JavaScript. These modern features make our code more expressive, concise, and maintainable.

Today, we'll explore the most important ES6+ features that you'll use frequently in modern web development, especially when working with WordPress and PHP. Think of ES6+ as a set of power tools added to your basic JavaScript toolbox—they don't replace the fundamentals but make complex jobs much easier.

The Evolution of JavaScript

Before diving into specific features, let's understand the context of JavaScript's evolution:

JavaScript Evolution Timeline 1995 JavaScript created 1997 ECMAScript 1 2009 ES5 2015 ES6/ES2015 Major Update 2016 ES2016 2017 ES2017 2018 ES2018 2020 ES2020 2022 ES2022 2024 ES2024 Key Milestones: Initial Releases ES6 - Revolutionary Update Annual Releases (ES2016+)

After the massive ES6 update in 2015, JavaScript now follows an annual release cycle with incremental improvements. When we talk about "ES6+", we mean ES6 and all subsequent versions combined.

Block-Scoped Declarations: let and const

Prior to ES6, we only had var for variable declarations, which had function scope. ES6 introduced let and const which provide block scope - a more intuitive way to handle variable access.

Traditional var vs Modern let/const

// The old way with var
var name = "John";
var age = 30;
if (true) {
  var name = "Jane"; // Overwrites the outer 'name'
  var greeting = "Hello";
}
console.log(name); // "Jane" - var is function scoped
console.log(greeting); // "Hello" - var leaks outside the block

// The new way with let and const
let name = "John";
const age = 30;
if (true) {
  let name = "Jane"; // Different variable, block-scoped
  const greeting = "Hello";
}
console.log(name); // "John" - outer variable unchanged
// console.log(greeting); // Error! 'greeting' not defined outside block

Real-World Application

In WordPress theme development, when you're writing JavaScript for multiple UI components on a page, block scoping with let and const prevents variable collisions between components.

// Navigation menu component
const navToggle = document.querySelector('.nav-toggle');
const navItems = document.querySelectorAll('.nav-item');

// Image gallery component on same page
const galleryImages = document.querySelectorAll('.gallery-image');
const lightbox = document.querySelector('.lightbox');

Because these variables are declared with const, they are scoped to their respective blocks and won't interfere with each other, even if you were to use the same variable names in different components.

Analogy: Variables as Containers

Think of var as a bucket that can be seen and modified from almost anywhere in your house (function), while let and const are like containers that only exist in specific rooms (blocks) of your house:

  • var - Large plastic bucket that can be carried anywhere in the house and its contents changed
  • let - Lunchbox that only exists in one room; contents can be changed
  • const - Sealed container that only exists in one room; contents cannot be changed after creation

Best Practices

  • Use const by default for most variables
  • Use let when you need to reassign a variable
  • Avoid var in modern code
  • Keep variables scoped as tightly as possible to prevent bugs

Template Literals

Template literals provide a cleaner way to create strings, especially when they include variables or span multiple lines.

Old vs. New String Creation

// Old way
var name = "Alice";
var greeting = "Hello, " + name + "!
" +
  "Welcome to our " + siteTitle + " website.
" +
  "You have " + notifications + " unread messages.";

// New way with template literals
const name = "Alice";
const greeting = `Hello, ${name}!
Welcome to our ${siteTitle} website.
You have ${notifications} unread messages.`;

Advantages of Template Literals

  1. Multi-line strings without concatenation or escape characters
  2. Expression interpolation with ${expression} syntax
  3. Cleaner code, especially for HTML templates

Real-World Example: Dynamic Content in WordPress

// Generating dynamic HTML for a WordPress plugin
function renderUserCard(user) {
  return `
    <div class="user-card" id="user-${user.id}">
      <img src="${user.avatar}" alt="${user.name}" class="avatar">
      <div class="user-info">
        <h3>${user.name}</h3>
        <p>${user.bio || 'No bio available'}</p>
        <p>Member since: ${formatDate(user.joinDate)}</p>
        ${user.isAdmin ? '<span class="badge admin-badge">Admin</span>' : ''}
      </div>
    </div>
  `;
}

Notice how clean the HTML structure remains even with dynamic content injected throughout.

Advanced: Tagged Template Literals

Template literals can be "tagged" with a function to process the template:

// A tag function for sanitizing HTML
function sanitize(strings, ...values) {
  return strings.reduce((result, string, i) => {
    const value = values[i - 1] || '';
    // Simple HTML sanitization
    const sanitizedValue = String(value)
      .replace(/&/g, '&')
      .replace(//g, '>')
      .replace(/"/g, '"');
    return result + sanitizedValue + string;
  });
}

// Example usage: registering multiple blocks with modern JS
const blocks = [
  { name: 'testimonial', title: 'Testimonial', icon: 'format-quote' },
  { name: 'team-member', title: 'Team Member', icon: 'admin-users' },
  { name: 'feature-box', title: 'Feature Box', icon: 'star-filled' }
];

// Using array methods and arrow functions
blocks.forEach(({ name, title, icon }) => {
  registerBlockType(`my-plugin/${name}`, {
    title,
    icon,
    category: 'common',
    // ... other block properties
  });
});

// Using the tag
const userName = '<script>alert("XSS")</script>';
const safeHTML = sanitize`<div>Hello, ${userName}</div>`;
// Result: <div>Hello, <script>alert("XSS")</script></div>
// Where the script tags are properly escaped

Arrow Functions

Arrow functions provide a more concise syntax for writing functions and solve common issues with the this keyword binding.

Traditional vs. Arrow Functions

// Traditional function expression
function add(a, b) {
  return a + b;
}

// Arrow function
const add = (a, b) => a + b;

// Traditional function with multiple statements
function greet(name) {
  const timeOfDay = getTimeOfDay();
  return `Good ${timeOfDay}, ${name}!`;
}

// Arrow function with multiple statements
const greet = (name) => {
  const timeOfDay = getTimeOfDay();
  return `Good ${timeOfDay}, ${name}!`;
};

The this Keyword and Arrow Functions

// Traditional functions create their own 'this'
const user = {
  name: 'John',
  greetTraditional: function() {
    setTimeout(function() {
      console.log('Hello, ' + this.name); // 'this' is not 'user'
    }, 1000);
  },
  greetArrow: function() {
    setTimeout(() => {
      console.log('Hello, ' + this.name); // 'this' is 'user'
    }, 1000);
  }
};

user.greetTraditional(); // Hello, undefined
user.greetArrow(); // Hello, John
Traditional Function function() {...} Creates new 'this' context Arrow Function () => {...} Inherits 'this' from outside

Real-World Example: Event Handlers in WordPress

// WordPress AJAX handler with arrow functions
class PostEditor {
  constructor() {
    this.postId = document.querySelector('#post-id').value;
    this.title = document.querySelector('#title');
    this.content = document.querySelector('#content');
    this.saveButton = document.querySelector('#save');
    
    // Arrow function preserves 'this' context
    this.saveButton.addEventListener('click', () => {
      this.savePost(); // 'this' refers to the PostEditor instance
    });
  }
  
  savePost() {
    const data = {
      action: 'save_post',
      post_id: this.postId,
      title: this.title.value,
      content: this.content.value
    };
    
    // Arrow function in callback
    jQuery.post(ajaxurl, data, (response) => {
      this.handleResponse(response); // 'this' still refers to PostEditor
    });
  }
  
  handleResponse(response) {
    // Process the server response
  }
}

When Not to Use Arrow Functions

  • Object methods (when you need to access the object via this)
  • Constructor functions (arrow functions cannot be used with new)
  • When you need access to the arguments object
  • Event handlers where this should refer to the event target

Destructuring Assignment

Destructuring allows you to extract values from arrays or properties from objects into distinct variables.

Object Destructuring

// Old way
var user = {
  name: 'Sarah',
  email: 'sarah@example.com',
  address: {
    city: 'Boston',
    state: 'MA'
  }
};
var name = user.name;
var email = user.email;
var city = user.address.city;

// With destructuring
const { name, email, address: { city, state } } = user;
console.log(name, email, city, state); // Sarah sarah@example.com Boston MA

Array Destructuring

// Old way
var coordinates = [10, 20, 30];
var x = coordinates[0];
var y = coordinates[1];
var z = coordinates[2];

// With destructuring
const [x, y, z] = coordinates;
console.log(x, y, z); // 10 20 30

// Skipping elements
const [first, , third] = ['apple', 'banana', 'cherry'];
console.log(first, third); // apple cherry

Default Values and Rest Patterns

// Default values
const { name, role = 'User' } = { name: 'Alex' };
console.log(name, role); // Alex User

// Rest pattern with arrays
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head, tail); // 1 [2, 3, 4, 5]

// Rest pattern with objects
const { id, ...userDetails } = { 
  id: 42, 
  name: 'Mia', 
  email: 'mia@example.com', 
  age: 28 
};
console.log(id, userDetails); 
// 42 { name: 'Mia', email: 'mia@example.com', age: 28 }

Real-World Example: WordPress REST API Response

// Processing WordPress REST API data
async function fetchPostDetails(postId) {
  const response = await fetch(`/wp-json/wp/v2/posts/${postId}`);
  const post = await response.json();
  
  // Destructure exactly what we need
  const { 
    title: { rendered: title }, 
    content: { rendered: content },
    author,
    featured_media: imageId,
    _embedded
  } = post;
  
  // Destructure author info from _embedded
  const { 
    author: [{ name: authorName, avatar_urls: { 96: authorAvatar } }] 
  } = _embedded;
  
  return {
    title,
    content,
    authorId: author,
    authorName,
    authorAvatar,
    imageId
  };
}

Destructuring makes it much cleaner to extract nested data from complex API responses, which is common when working with WordPress's REST API.

Destructuring in Function Parameters

// Without destructuring
function displayUser(user) {
  console.log(`${user.name} (${user.email})`);
}

// With destructuring
function displayUser({ name, email }) {
  console.log(`${name} (${email})`);
}

// With default values
function createWidget({ width = 300, height = 200, color = 'blue' } = {}) {
  // The = {} ensures the function works even when called with no arguments
  console.log(`Creating a ${color} widget: ${width}x${height}px`);
}

Spread and Rest Operators

The ... syntax serves two purposes: spreading elements and collecting them into arrays or objects.

Spread Syntax

// Spread in arrays
const fruits = ['apple', 'banana'];
const moreFruits = [...fruits, 'cherry', 'date'];
console.log(moreFruits); // ['apple', 'banana', 'cherry', 'date']

// Spread in objects
const baseConfig = { 
  theme: 'dark', 
  animationEnabled: true 
};
const userConfig = { 
  theme: 'light', 
  notifications: true 
};
const finalConfig = { ...baseConfig, ...userConfig };
console.log(finalConfig); 
// { theme: 'light', animationEnabled: true, notifications: true }

Note that with objects, properties from later spreads will overwrite earlier ones with the same name.

Common Array Operations with Spread

// Copy an array
const original = [1, 2, 3];
const copy = [...original];

// Concatenate arrays
const arr1 = [1, 2];
const arr2 = [3, 4];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4]

// Convert string to array of characters
const chars = [...'hello']; // ['h', 'e', 'l', 'l', 'o']

// Function arguments
const numbers = [1, 2, 3];
console.log(Math.max(...numbers)); // 3

Rest Operator

// Rest in function parameters
function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

// Rest with destructuring
const [first, second, ...remaining] = [1, 2, 3, 4, 5];
console.log(first, second, remaining); // 1 2 [3, 4, 5]

Real-World Example: WordPress Theme Options

// Merging theme default options with user options
function initThemeOptions(userOptions = {}) {
  const defaultOptions = {
    colorScheme: 'light',
    fontSize: 'medium',
    sidebarPosition: 'right',
    headerStyle: 'minimal',
    footerWidgets: true,
    socialIcons: ['facebook', 'twitter'],
    performance: {
      lazyLoading: true,
      minifyCss: true,
      minifyJs: true
    }
  };
  
  // Deep merge with user options
  const options = {
    ...defaultOptions,
    ...userOptions,
    // Deep merge for nested objects
    performance: {
      ...defaultOptions.performance,
      ...(userOptions.performance || {})
    },
    // Combine arrays instead of replacing
    socialIcons: [
      ...defaultOptions.socialIcons,
      ...(userOptions.socialIcons || [])
    ]
  };
  
  return options;
}

// Usage
const userSelectedOptions = {
  colorScheme: 'dark',
  fontSize: 'large',
  socialIcons: ['instagram'],
  performance: {
    minifyJs: false
  }
};

const finalOptions = initThemeOptions(userSelectedOptions);
// Result includes defaults + user options
Spread Operator Arrays [...array] Objects {...object} Function calls fn(...args) Rest Operator Function params (...args) [a, ...rest] {a, ...rest}

Default Parameters

ES6 introduced a clean way to specify default values for function parameters.

Before vs. After

// Old way
function createProfile(name, age, isPremium) {
  name = name || 'Anonymous';
  age = age !== undefined ? age : 30;
  isPremium = isPremium !== undefined ? isPremium : false;
  
  return {
    name: name,
    age: age,
    accountType: isPremium ? 'Premium' : 'Basic'
  };
}

// ES6 way
function createProfile(name = 'Anonymous', age = 30, isPremium = false) {
  return {
    name,
    age,
    accountType: isPremium ? 'Premium' : 'Basic'
  };
}

Expressions as Default Values

// Default parameters can be expressions
function getTimestamp(date = new Date()) {
  return date.getTime();
}

// They can use previous parameters
function createRange(start = 1, end = start + 10) {
  return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
console.log(createRange(5)); // [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

Real-World Example: WordPress Shortcode Generator

// Generating shortcodes with default attributes
function generateGalleryShortcode({
  ids = [],
  columns = 3,
  size = 'medium',
  link = 'file',
  orderby = 'menu_order',
  order = 'ASC'
} = {}) {
  // Validate and process IDs
  if (!Array.isArray(ids) || ids.length === 0) {
    return ''; // Can't create gallery without images
  }
  
  const attributes = {
    ids: ids.join(','),
    columns,
    size,
    link,
    orderby,
    order
  };
  
  // Generate shortcode
  const attrString = Object.entries(attributes)
    .map(([key, value]) => `${key}="${value}"`)
    .join(' ');
  
  return `[gallery ${attrString}]`;
}

// Usage examples
generateGalleryShortcode({ ids: [101, 102, 103] });
// [gallery ids="101,102,103" columns="3" size="medium" link="file" orderby="menu_order" order="ASC"]

generateGalleryShortcode({ ids: [101, 102], size: 'large', columns: 2 });
// [gallery ids="101,102" columns="2" size="large" link="file" orderby="menu_order" order="ASC"]

Classes and Object-Oriented Programming

ES6 introduced a class syntax that makes it easier to implement object-oriented programming patterns in JavaScript.

Traditional Prototype vs. ES6 Class

// Pre-ES6 constructor function and prototype
function User(name, email) {
  this.name = name;
  this.email = email;
  this.createdAt = new Date();
}

User.prototype.getProfile = function() {
  return `${this.name} (${this.email})`;
};

User.prototype.sendEmail = function(subject, body) {
  console.log(`Sending email to ${this.email}: ${subject}`);
  // Email sending logic
};

// ES6 Class syntax
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
    this.createdAt = new Date();
  }
  
  getProfile() {
    return `${this.name} (${this.email})`;
  }
  
  sendEmail(subject, body) {
    console.log(`Sending email to ${this.email}: ${subject}`);
    // Email sending logic
  }
}

Inheritance with extends

// Base class
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
    this.role = 'user';
  }
  
  hasPermission(permission) {
    return false; // Basic users have no special permissions
  }
}

// Derived class
class Admin extends User {
  constructor(name, email) {
    super(name, email); // Call parent constructor
    this.role = 'admin';
  }
  
  hasPermission(permission) {
    return true; // Admins have all permissions
  }
  
  resetUserPassword(userId) {
    console.log(`Admin ${this.name} resetting password for user ${userId}`);
    // Password reset logic
  }
}

Static Methods and Properties

class MathUtils {
  // Static method
  static sum(...numbers) {
    return numbers.reduce((total, num) => total + num, 0);
  }
  
  static multiply(...numbers) {
    return numbers.reduce((product, num) => product * num, 1);
  }
  
  // Static property (ES2022+)
  static PI = 3.14159265359;
}

console.log(MathUtils.sum(1, 2, 3, 4)); // 10
console.log(MathUtils.PI); // 3.14159265359

Getters and Setters

class Circle {
  constructor(radius) {
    this._radius = radius;
  }
  
  // Getter
  get radius() {
    return this._radius;
  }
  
  // Setter
  set radius(value) {
    if (value <= 0) {
      throw new Error('Radius must be positive');
    }
    this._radius = value;
  }
  
  // Getter
  get area() {
    return Math.PI * this._radius * this._radius;
  }
  
  // Getter
  get circumference() {
    return 2 * Math.PI * this._radius;
  }
}

const circle = new Circle(5);
console.log(circle.radius); // 5
console.log(circle.area); // ~78.54
circle.radius = 10;
console.log(circle.area); // ~314.16

Real-World Example: WordPress Plugin Architecture

// Base Plugin class
class WPPlugin {
  constructor(pluginName, version) {
    this.pluginName = pluginName;
    this.version = version;
    this.actions = [];
    this.filters = [];
  }
  
  init() {
    this.registerHooks();
    console.log(`${this.pluginName} v${this.version} initialized`);
  }
  
  registerHooks() {
    // To be implemented by subclasses
  }
  
  addAction(hook, callback, priority = 10) {
    this.actions.push({ hook, callback, priority });
    // WordPress equivalent: add_action(hook, callback, priority);
  }
  
  addFilter(hook, callback, priority = 10) {
    this.filters.push({ hook, callback, priority });
    // WordPress equivalent: add_filter(hook, callback, priority);
  }
  
  // Static helper method
  static sanitizeHtml(html) {
    // Sanitization logic
    return html.replace(/<script[^<]*(?:(?!</script>)<[^<]*)*</script>/gi, '');

  }
}

// Specific plugin implementation
class ContactFormPlugin extends WPPlugin {
  constructor() {
    super('Contact Form Plus', '1.2.0');
    this.formFields = [
      { name: 'name', label: 'Full Name', required: true },
      { name: 'email', label: 'Email Address', required: true },
      { name: 'message', label: 'Message', type: 'textarea', required: true }
    ];
  }
  
  registerHooks() {
    this.addAction('wp_enqueue_scripts', () => this.enqueueAssets());
    this.addAction('init', () => this.registerShortcode());
    this.addAction('wp_ajax_submit_contact', () => this.processForm());
    this.addAction('wp_ajax_nopriv_submit_contact', () => this.processForm());
  }
  
  enqueueAssets() {
    // Enqueue scripts and styles
  }
  
  registerShortcode() {
    // Register [contact_form] shortcode
  }
  
  renderForm() {
    // Render the contact form HTML
    return `
      <form class="contact-form" method="post">
        ${this.formFields.map(field => this.renderField(field)).join('')}
        <button type="submit">Send Message</button>
      </form>
    `;
  }
  
  renderField(field) {
    // Render individual form field
  }
  
  processForm() {
    // Process form submission
  }
}

// Usage
const contactForm = new ContactFormPlugin();
contactForm.init();

This example shows how ES6 classes provide a clean way to structure WordPress plugins with inheritance, encapsulation, and code organization.

Private Class Features (ES2022+)

class BankAccount {
  // Private field
  #balance = 0;
  #transactionHistory = [];
  
  constructor(initialDeposit = 0) {
    if (initialDeposit > 0) {
      this.deposit(initialDeposit);
    }
  }
  
  // Public methods
  deposit(amount) {
    if (amount <= 0) throw new Error('Deposit amount must be positive');
    this.#balance += amount;
    this.#addTransaction('deposit', amount);
    return this.#balance;
  }
  
  withdraw(amount) {
    if (amount <= 0) throw new Error('Withdrawal amount must be positive');
    if (amount > this.#balance) throw new Error('Insufficient funds');
    
    this.#balance -= amount;
    this.#addTransaction('withdrawal', amount);
    return this.#balance;
  }
  
  getBalance() {
    return this.#balance;
  }
  
  getTransactionHistory() {
    // Return a copy to prevent modification
    return [...this.#transactionHistory];
  }
  
  // Private method
  #addTransaction(type, amount) {
    this.#transactionHistory.push({
      type,
      amount,
      timestamp: new Date(),
      balance: this.#balance
    });
  }
}

const account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
console.log(account.getTransactionHistory()); // Transaction history

// These would throw errors:
// console.log(account.#balance); // SyntaxError
// account.#addTransaction('hack', 1000000); // SyntaxError
Class Inheritance Diagram User + name: String + email: String + createdAt: Date + getProfile() + sendEmail() Admin + resetUserPassword() + hasPermission() Editor + permissions: Array + hasPermission() + editContent() Legend: Inherits from

Promises and Async/Await

Modern JavaScript provides powerful ways to handle asynchronous operations, making code cleaner and more maintainable.

Promises

// Creating a promise
const fetchUserData = (userId) => {
  return new Promise((resolve, reject) => {
    // Simulating API call
    setTimeout(() => {
      if (userId > 0) {
        resolve({
          id: userId,
          name: 'User ' + userId,
          email: `user${userId}@example.com`
        });
      } else {
        reject(new Error('Invalid user ID'));
      }
    }, 1000);
  });
};

// Using promises
fetchUserData(42)
  .then(user => {
    console.log('User data:', user);
    return fetchUserPosts(user.id);
  })
  .then(posts => {
    console.log('User posts:', posts);
  })
  .catch(error => {
    console.error('Error:', error.message);
  })
  .finally(() => {
    console.log('Operation completed');
  });

Async/Await (ES2017)

// Async function
async function getUserAndPosts(userId) {
  try {
    // Await pauses execution until promise resolves
    const user = await fetchUserData(userId);
    console.log('User data:', user);
    
    const posts = await fetchUserPosts(user.id);
    console.log('User posts:', posts);
    
    return { user, posts };
  } catch (error) {
    console.error('Error:', error.message);
    throw error; // Re-throw if needed
  } finally {
    console.log('Operation completed');
  }
}

// Using an async function
getUserAndPosts(42)
  .then(result => {
    console.log('Everything loaded successfully');
  })
  .catch(error => {
    console.log('Something went wrong');
  });
Promise Lifecycle Flow Promise Creation Pending State Result? Success Error Resolved Rejected then handlers catch handlers Promise Chain

Parallel Promise Operations

// Promise.all - waits for all promises to resolve
async function loadDashboardData(userId) {
  try {
    // Run these operations in parallel
    const [user, posts, comments, stats] = await Promise.all([
      fetchUserData(userId),
      fetchUserPosts(userId),
      fetchUserComments(userId),
      fetchUserStats(userId)
    ]);
    
    return {
      user,
      posts,
      comments,
      stats
    };
  } catch (error) {
    console.error('Error loading dashboard:', error);
    throw error;
  }
}

// Promise.race - resolves as soon as one promise resolves
async function fetchWithTimeout(url, timeoutMs) {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Request timed out')), timeoutMs);
  });
  
  const responsePromise = fetch(url);
  
  // Whichever finishes first wins the race
  return Promise.race([responsePromise, timeoutPromise]);
}

Real-World Example: WordPress REST API Interactions

// WordPress theme/plugin REST API interactions
class WPAPIClient {
  constructor(baseUrl = '/wp-json/wp/v2') {
    this.baseUrl = baseUrl;
    this.defaultHeaders = {
      'Content-Type': 'application/json',
      'X-WP-Nonce': wpApiSettings.nonce // WordPress provided nonce
    };
  }
  
  // Get data from API
  async get(endpoint, params = {}) {
    const url = new URL(this.baseUrl + endpoint, window.location.origin);
    
    // Add query parameters
    Object.entries(params).forEach(([key, value]) => {
      url.searchParams.append(key, value);
    });
    
    try {
      const response = await fetch(url, {
        method: 'GET',
        headers: this.defaultHeaders,
        credentials: 'same-origin'
      });
      
      if (!response.ok) {
        throw new Error(`API error: ${response.status}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error(`Error fetching ${endpoint}:`, error);
      throw error;
    }
  }
  
  // Create data via API
  async post(endpoint, data = {}) {
    try {
      const response = await fetch(this.baseUrl + endpoint, {
        method: 'POST',
        headers: this.defaultHeaders,
        credentials: 'same-origin',
        body: JSON.stringify(data)
      });
      
      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || `API error: ${response.status}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error(`Error posting to ${endpoint}:`, error);
      throw error;
    }
  }
  
  // Update data via API
  async update(endpoint, id, data = {}) {
    return this.post(`${endpoint}/${id}`, data);
  }
  
  // Delete data via API
  async delete(endpoint, id) {
    try {
      const response = await fetch(`${this.baseUrl}${endpoint}/${id}`, {
        method: 'DELETE',
        headers: {
          ...this.defaultHeaders,
          'X-HTTP-Method-Override': 'DELETE'
        },
        credentials: 'same-origin'
      });
      
      if (!response.ok) {
        throw new Error(`API error: ${response.status}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error(`Error deleting ${endpoint}/${id}:`, error);
      throw error;
    }
  }
}

// Usage example: Building a post editor
async function initPostEditor() {
  const api = new WPAPIClient();
  const editor = {
    postId: document.getElementById('post-id').value,
    titleField: document.getElementById('title'),
    contentField: document.getElementById('content'),
    saveButton: document.getElementById('save'),
    statusMessage: document.getElementById('status')
  };
  
  try {
    // Load existing post data
    if (editor.postId) {
      const post = await api.get(`/posts/${editor.postId}`);
      editor.titleField.value = post.title.raw;
      editor.contentField.value = post.content.raw;
    }
    
    // Set up save handler
    editor.saveButton.addEventListener('click', async () => {
      try {
        editor.statusMessage.textContent = 'Saving...';
        editor.saveButton.disabled = true;
        
        const postData = {
          title: editor.titleField.value,
          content: editor.contentField.value,
          status: 'publish'
        };
        
        let result;
        if (editor.postId) {
          result = await api.update('/posts', editor.postId, postData);
        } else {
          result = await api.post('/posts', postData);
          // Update URL with new post ID
          window.history.replaceState(
            {}, 
            document.title, 
            `?post=${result.id}&action=edit`
          );
        }
        
        editor.statusMessage.textContent = 'Post saved successfully!';
        editor.postId = result.id;
      } catch (error) {
        editor.statusMessage.textContent = `Error: ${error.message}`;
      } finally {
        editor.saveButton.disabled = false;
      }
    });
  } catch (error) {
    console.error('Error initializing editor:', error);
    editor.statusMessage.textContent = 'Error loading post data';
  }
}

// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', initPostEditor);

This example shows how Promises and async/await provide a clean way to handle complex asynchronous operations when building interactive WordPress interfaces.

Additional Promise Patterns (ES2020+)

// Promise.allSettled - get results of all promises, even rejected ones
async function fetchAllUserData(userIds) {
  const results = await Promise.allSettled(
    userIds.map(id => fetchUserData(id))
  );
  
  // Process all results, including errors
  const successfulFetches = results
    .filter(result => result.status === 'fulfilled')
    .map(result => result.value);
    
  const failedFetches = results
    .filter(result => result.status === 'rejected')
    .map(result => result.reason);
    
  console.log(`Successfully fetched: ${successfulFetches.length} users`);
  console.log(`Failed to fetch: ${failedFetches.length} users`);
  
  return {
    successful: successfulFetches,
    failed: failedFetches
  };
}

// Promise.any - resolve when any promise resolves (ES2021)
async function fetchFromFastestMirror(urls) {
  try {
    const response = await Promise.any(
      urls.map(url => fetch(url))
    );
    return await response.json();
  } catch (error) {
    // AggregateError if all promises reject
    console.error('All mirrors failed:', error);
    throw new Error('Could not fetch data from any mirror');
  }
}

ES Modules

ES6 introduced a standardized module system for organizing and sharing JavaScript code.

Basic Module Syntax

// utils.js - exporting functionality
export function formatDate(date) {
  return new Intl.DateTimeFormat('en-US').format(date);
}

export function formatCurrency(amount) {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD'
  }).format(amount);
}

export const TAX_RATE = 0.07;

// main.js - importing functionality
import { formatDate, formatCurrency, TAX_RATE } from './utils.js';

const orderDate = new Date();
const orderAmount = 99.99;
const tax = orderAmount * TAX_RATE;
const total = orderAmount + tax;

console.log(`Order Date: ${formatDate(orderDate)}`);
console.log(`Subtotal: ${formatCurrency(orderAmount)}`);
console.log(`Tax: ${formatCurrency(tax)}`);
console.log(`Total: ${formatCurrency(total)}`);

Default Exports and Imports

// user.js
export default class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
  
  getProfile() {
    return `${this.name} (${this.email})`;
  }
}

// You can also export constants, functions, etc. as default
export default function createUser(name, email) {
  return new User(name, email);
}

// Importing a default export
import User from './user.js';
const user = new User('John', 'john@example.com');

Mixed Default and Named Exports

// api.js
export default class API {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }
  
  // API methods...
}

export const API_VERSION = '1.0.0';
export function formatResponse(data) {
  // Format logic...
}

// Importing mixed exports
import API, { API_VERSION, formatResponse } from './api.js';

Renaming Imports and Exports

// math.js
export const PI = 3.14159;
export const E = 2.71828;

// Renaming during export
export { PI as MATH_PI, E as EULER_NUMBER };

// Importing with different names
import { 
  PI, 
  E,
  MATH_PI as π,
  EULER_NUMBER as e
} from './math.js';

Dynamic Imports (ES2020)

// Lazy-load modules when needed
async function loadEditor() {
  try {
    // Load module dynamically when needed
    const { default: Editor } = await import('./editor.js');
    
    // Initialize editor
    const editor = new Editor('#content');
    editor.init();
    
    return editor;
  } catch (error) {
    console.error('Error loading editor:', error);
  }
}

// Use when needed
document.querySelector('#edit-button').addEventListener('click', () => {
  loadEditor();
});

Real-World Example: WordPress Plugin Architecture


        // src/utils/formatting.js
export function formatDate(date) {
  return new Date(date).toLocaleDateString();
}

export function truncateText(text, length = 100) {
  if (text.length <= length) return text;
  return text.substring(0, length) + '...';
}

// src/components/PostCard.js
import { formatDate, truncateText } from '../utils/formatting.js';

export default class PostCard {
  constructor(post) {
    this.post = post;
  }
  
  render() {
    return `
      <div class="post-card" id="post-${this.post.id}">
        <h3>${this.post.title}</h3>
        <div class="post-meta">
          <span class="date">${formatDate(this.post.date)}</span>
          <span class="author">by ${this.post.author}</span>
        </div>
        <div class="post-excerpt">
          ${truncateText(this.post.content, 150)}
        </div>
        <a href="${this.post.url}" class="read-more">Read More</a>
      </div>
    `;
  }
}

// src/components/PostList.js
import PostCard from './PostCard.js';

export default class PostList {
  constructor(container, posts = []) {
    this.container = document.querySelector(container);
    this.posts = posts;
  }
  
  async loadPosts() {
    try {
      const response = await fetch('/wp-json/wp/v2/posts');
      this.posts = await response.json();
      this.render();
    } catch (error) {
      console.error('Error loading posts:', error);
    }
  }
  
  render() {
    if (!this.container) return;
    
    this.container.innerHTML = this.posts
      .map(post => new PostCard(post).render())
      .join('');
  }
}

// src/index.js - Main entry point
import PostList from './components/PostList.js';

document.addEventListener('DOMContentLoaded', () => {
  const postList = new PostList('#blog-posts');
  postList.loadPosts();
});

This modular approach makes it easier to organize code, reuse components, and maintain WordPress themes and plugins.

Using ES Modules with WordPress

To use ES modules in WordPress, you'll need to:

  1. Enqueue your script with the type="module" attribute:
    // In your theme's functions.php or plugin file
    function enqueue_module_scripts() {
      wp_enqueue_script(
        'my-module-script',
        get_template_directory_uri() . '/js/module.js',
        [],
        '1.0.0',
        true
      );
      
      // Add type="module" attribute
      add_filter('script_loader_tag', function($tag, $handle, $src) {
        if ($handle === 'my-module-script') {
          $tag = '<script type="module" src="' . esc_url($src) . '"></script>';
        }
        return $tag;
      }, 10, 3);
    }
    add_action('wp_enqueue_scripts', 'enqueue_module_scripts');
  2. Consider browser compatibility and provide fallbacks for older browsers
  3. Use a bundler like Webpack or Rollup during development for older browsers

Other Important ES6+ Features

Map and Set Collections

// Map - key-value pairs where keys can be any type
const userRoles = new Map();
userRoles.set(42, 'admin');
userRoles.set('jane@example.com', 'editor');
userRoles.set(user1, 'contributor'); // Can use objects as keys

console.log(userRoles.get(42)); // 'admin'
console.log(userRoles.has('jane@example.com')); // true
console.log(userRoles.size); // 3

// Set - collection of unique values
const uniqueCategories = new Set();
uniqueCategories.add('JavaScript');
uniqueCategories.add('PHP');
uniqueCategories.add('WordPress');
uniqueCategories.add('JavaScript'); // Duplicate, won't be added

console.log(uniqueCategories.size); // 3
console.log(uniqueCategories.has('PHP')); // true

// Convert Set to Array
const categoriesArray = [...uniqueCategories];

Symbol - Unique Identifiers

// Create unique property keys
const id = Symbol('id');
const user = {
  name: 'Alice',
  [id]: 42 // Using a Symbol as a property key
};

console.log(user[id]); // 42
console.log(Object.keys(user)); // ['name'] - Symbols are not enumerable

// Well-known Symbols
class CustomCollection {
  constructor() {
    this.items = [];
  }
  
  add(item) {
    this.items.push(item);
  }
  
  // Make the class iterable
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.items.length) {
          return { value: this.items[index++], done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
}

const collection = new CustomCollection();
collection.add('a');
collection.add('b');
collection.add('c');

// Now we can use for...of
for (const item of collection) {
  console.log(item); // 'a', 'b', 'c'
}

Array Methods

// Array.from - create arrays from array-like objects
const nodeList = document.querySelectorAll('.item');
const itemsArray = Array.from(nodeList);

// With mapping function
const numbersAsStrings = ['1', '2', '3', '4'];
const numbers = Array.from(numbersAsStrings, str => parseInt(str, 10));

// Array.find and Array.findIndex
const posts = [
  { id: 1, title: 'Hello World', status: 'published' },
  { id: 2, title: 'Draft Post', status: 'draft' },
  { id: 3, title: 'Another Post', status: 'published' }
];

const publishedPost = posts.find(post => post.status === 'published');
// { id: 1, title: 'Hello World', status: 'published' }

const draftIndex = posts.findIndex(post => post.status === 'draft');
// 1

// Array.includes
const categories = ['JavaScript', 'PHP', 'WordPress'];
console.log(categories.includes('PHP')); // true

// Array.flat and Array.flatMap (ES2019)
const nestedArray = [1, 2, [3, 4, [5, 6]]];
console.log(nestedArray.flat()); // [1, 2, 3, 4, [5, 6]]
console.log(nestedArray.flat(2)); // [1, 2, 3, 4, 5, 6]

// flatMap combines map and flat
const sentences = ['Hello world', 'How are you'];
const words = sentences.flatMap(sentence => sentence.split(' '));
// ['Hello', 'world', 'How', 'are', 'you']

Object Methods

// Object.entries and Object.fromEntries
const person = { name: 'John', age: 30, role: 'developer' };

// Convert object to array of [key, value] pairs
const entries = Object.entries(person);
// [['name', 'John'], ['age', 30], ['role', 'developer']]

// Process entries
const processedEntries = entries.map(([key, value]) => {
  if (key === 'age') return [key, value + 1];
  return [key, value];
});

// Convert back to object
const updatedPerson = Object.fromEntries(processedEntries);
// { name: 'John', age: 31, role: 'developer' }

// Object.values
const values = Object.values(person);
// ['John', 30, 'developer']

// Object spread (ES2018)
const defaults = { theme: 'light', notifications: true, fontSize: 'medium' };
const userPreferences = { theme: 'dark', fontSize: 'large' };

const settings = { ...defaults, ...userPreferences };
// { theme: 'dark', notifications: true, fontSize: 'large' }

String Methods

// String.padStart and String.padEnd
const productId = '42';
const paddedId = productId.padStart(5, '0'); // '00042'

const fileName = 'report';
const fullFileName = fileName.padEnd(10, '_') + '.pdf'; // 'report_____.pdf'

// String.trimStart and String.trimEnd
const input = '  user@example.com  ';
const trimmedInput = input.trim(); // 'user@example.com'
const trimmedStart = input.trimStart(); // 'user@example.com  '
const trimmedEnd = input.trimEnd(); // '  user@example.com'

// String.replaceAll (ES2021)
const template = 'Hello {{name}}, welcome to {{site}}!';
const message = template
  .replaceAll('{{name}}', 'John')
  .replaceAll('{{site}}', 'WordPress Dev Course');
// 'Hello John, welcome to WordPress Dev Course!'

Nullish Coalescing and Optional Chaining (ES2020)

// Nullish coalescing operator (??)
// Only falls back if value is null or undefined
function getUserSettings(userId) {
  const user = getUser(userId);
  
  // Old way: might fallback unintentionally for "" or 0 or false
  const username = user.username || 'Anonymous';
  const postsPerPage = user.preferences.postsPerPage || 10;
  
  // New way: only falls back for null/undefined
  const username = user.username ?? 'Anonymous';
  // If user has postsPerPage: 0, this will correctly use 0
  const postsPerPage = user.preferences.postsPerPage ?? 10;
}

// Optional chaining (?.)
// Prevents errors when accessing properties of undefined
function displayUserProfile(userId) {
  const user = getUser(userId);
  
  // Old way: verbose checks to avoid errors
  const city = user && user.address && user.address.city;
  
  // New way: concise and safe property access
  const city = user?.address?.city;
  
  // Also works with methods
  user?.sendEmail?.('Welcome!');
  
  // And array access
  const firstTag = user?.tags?.[0];
}

Practical Application: Modern JavaScript in WordPress Development

WordPress Development Architecture WordPress Development Backend PHP and WP Core Frontend JavaScript and ES6 Theme Development Plugin Development Gutenberg Blocks REST API Interactions Admin UI Enhancements

Modern JavaScript is essential for WordPress development, especially since the introduction of the Block Editor (Gutenberg) which is built with React and relies heavily on ES6+ features.

Where ES6+ Features Are Used in WordPress

  • Block Editor (Gutenberg) - Built with React, uses ES6+ extensively
  • Theme Development - Modern themes use ES6 for interactive features
  • Custom Blocks - Block development relies on ES6 classes, async/await, etc.
  • AJAX Interactions - Fetch API and Promises for server communication
  • Plugin Development - Modern plugins use ES6+ for organization and maintainability

Example: Creating a Custom Gutenberg Block

The following example demonstrates how various ES6+ features (arrow functions, destructuring, template literals, etc.) are used when creating a custom testimonial block for the WordPress Gutenberg editor:

// blocks/testimonial/index.js
          import { registerBlockType } from '@wordpress/blocks';
          import { RichText, MediaUpload, InspectorControls } from '@wordpress/block-editor';
          import { PanelBody, TextControl, SelectControl } from '@wordpress/components';
          
          // Register block using ES6+ features
          registerBlockType('my-plugin/testimonial', {
            title: 'Testimonial',
            icon: 'format-quote',
            category: 'common',
            
            // Using object destructuring in parameters
            attributes: {
              quote: {
                type: 'string',
                source: 'html',
                selector: '.testimonial-text'
              },
              author: {
                type: 'string',
                source: 'html',
                selector: '.testimonial-author'
              },
              avatarUrl: {
                type: 'string',
                default: ''
              },
              backgroundColor: {
                type: 'string',
                default: '#f7f7f7'
              }
            },
            
            // Arrow function with destructured parameters
            edit: ({ attributes, setAttributes }) => {
              const { quote, author, avatarUrl, backgroundColor } = attributes;
              
              // Using template literals for inline styles
              const blockStyle = {
                backgroundColor,
                padding: '20px',
                borderRadius: '4px'
              };
              
              // Event handler using arrow function
              const onChangeQuote = (newQuote) => {
                setAttributes({ quote: newQuote });
              };
              
              const onChangeAuthor = (newAuthor) => {
                setAttributes({ author: newAuthor });
              };
              
              const onSelectImage = (media) => {
                setAttributes({ avatarUrl: media.url });
              };
              
              const onChangeBackgroundColor = (newColor) => {
                setAttributes({ backgroundColor: newColor });
              };
              
              // Using JSX (transpiled from ES6+)
              return (
                
  
    
  


{avatarUrl && ( Author avatar )} ( )} />
// Using template literals in the save function save: ({ attributes }) => { const { quote, author, avatarUrl, backgroundColor } = attributes; return (
{quote}
{avatarUrl && ( Author avatar )} {author}
); } })

Performance Tips When Using ES6+ in WordPress

  • Bundle size awareness - Modern features can increase file size when transpiled
  • Code splitting - Use dynamic imports to load code only when needed
  • Transpilation - Use Babel to ensure compatibility with older browsers
  • Minification - Use terser or similar tools to reduce file size
  • Polyfills - Include only necessary polyfills for features you use

Example: AJAX with Fetch and async/await

// modern-admin.js - WordPress admin UI enhancement
        class PostManager {
          constructor() {
            this.posts = [];
            this.container = document.querySelector('#posts-container');
            this.filterForm = document.querySelector('#post-filter-form');
            
            // Event binding with arrow functions to maintain 'this' context
            this.filterForm.addEventListener('submit', (e) => this.handleFilterSubmit(e));
            
            // Initialize
            this.init();
          }
          
          async init() {
            try {
              await this.loadPosts();
              this.renderPosts();
            } catch (error) {
              this.showError('Failed to load posts.');
              console.error(error);
            }
          }
          
          // Using async/await with the Fetch API
          async loadPosts(filters = {}) {
            this.showLoading();
            
            // Build query parameters using Object methods
            const queryParams = new URLSearchParams(
              Object.entries(filters).filter(([_, value]) => value)
            ).toString();
            
            try {
              const response = await fetch(`/wp-json/wp/v2/posts?${queryParams}`);
              
              if (!response.ok) {
                throw new Error(`API error: ${response.status}`);
              }
              
              this.posts = await response.json();
              return this.posts;
            } catch (error) {
              console.error('Error fetching posts:', error);
              throw error;
            } finally {
              this.hideLoading();
            }
          }
          
          // Using template literals for HTML generation
          renderPosts() {
            if (!this.posts.length) {
              this.container.innerHTML = '<p>No posts found.</p>';
              return;
            }
            
            // Using map and join for HTML generation
            this.container.innerHTML = this.posts
              .map(post => `
                <div class="post-card" id="post-${post.id}">
                  <h3>${post.title.rendered}</h3>
                  <div class="post-meta">
                    ${new Date(post.date).toLocaleDateString()}
                  </div>
                  <div class="post-actions">
                    <button data-id="${post.id}" class="edit-post">Edit</button>
                    <button data-id="${post.id}" class="delete-post">Delete</button>
                  </div>
                </div>
              `)
              .join('');
            
            // Event delegation with arrow functions
            this.container.addEventListener('click', e => {
              if (e.target.classList.contains('edit-post')) {
                const postId = e.target.dataset.id;
                this.editPost(postId);
              } else if (e.target.classList.contains('delete-post')) {
                const postId = e.target.dataset.id;
                this.confirmDeletePost(postId);
              }
            });
          }
          
          async handleFilterSubmit(e) {
            e.preventDefault();
            
            // FormData and Object.fromEntries for form processing
            const formData = new FormData(this.filterForm);
            const filters = Object.fromEntries(formData);
            
            try {
              await this.loadPosts(filters);
              this.renderPosts();
            } catch (error) {
              this.showError('Error applying filters.');
            }
          }
          
          // More methods omitted for brevity...
          
          showLoading() {
            // Show loading indicator
          }
          
          hideLoading() {
            // Hide loading indicator
          }
          
          showError(message) {
            // Display error message
          }
        }
        
        // Initialize when DOM is ready
        document.addEventListener('DOMContentLoaded', () => {
          new PostManager();
        });

Browser Compatibility and Transpilation

While ES6+ features are powerful, not all browsers (especially older ones) support them natively. This is particularly important for WordPress developers who need to support a wide range of users.

Build Process for ES6+ Code Modern ES6 Code Babel Transpiler ES5 Compatible Code Browser Execution Dev Dependencies Webpack or Gulp Bundling Process Flow: Source Code → Transpilation → Bundling → Deployment

Setting Up Transpilation for WordPress Projects

// package.json example for a WordPress theme/plugin
        {
          "name": "my-wordpress-project",
          "version": "1.0.0",
          "scripts": {
            "build": "webpack --mode production",
            "dev": "webpack --mode development --watch"
          },
          "devDependencies": {
            "@babel/core": "^7.20.5",
            "@babel/preset-env": "^7.20.2",
            "babel-loader": "^9.1.0",
            "webpack": "^5.75.0",
            "webpack-cli": "^5.0.1"
          }
        }
        
        // webpack.config.js
        const path = require('path');
        
        module.exports = {
          entry: {
            main: './src/js/main.js',
            admin: './src/js/admin.js'
          },
          output: {
            filename: '[name].js',
            path: path.resolve(__dirname, 'dist/js')
          },
          module: {
            rules: [
              {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                  loader: 'babel-loader',
                  options: {
                    presets: ['@babel/preset-env']
                  }
                }
              }
            ]
          }
        };

Compatibility Strategies

  1. Feature detection - Check if features exist before using them
  2. Polyfills - Add missing functionality for older browsers
  3. Progressive enhancement - Provide basic functionality for all users, enhanced for modern browsers
// Feature detection example
        if (window.fetch) {
          // Use fetch API
        } else {
          // Fall back to XMLHttpRequest
        }
        
        // Using core-js for polyfills (imported selectively)
        import 'core-js/features/promise';
        import 'core-js/features/object/entries';
        import 'core-js/features/array/from';

Learning Resources and Tools

Essential Resources

  • MDN Web Docs - Comprehensive JavaScript reference
  • Babel - The JavaScript compiler/transpiler
  • ESLint - Code quality tool that helps enforce ES6+ best practices
  • WordPress Coding Standards - JavaScript guidelines for WordPress development
  • WordPress Block Editor Handbook - Documentation for Gutenberg development

Development Tools

  • webpack - Module bundler for JavaScript applications
  • npm/yarn - Package managers for JavaScript libraries
  • VS Code - Editor with excellent ES6+ support and extensions
  • Chrome DevTools - For debugging and performance analysis
  • @wordpress/scripts - WordPress package for common JS tools configuration

Recap and Summary

Key Takeaways

  • ES6+ introduced powerful features that make JavaScript more expressive and maintainable
  • Block-scoped variables (let and const) provide better scoping control than var
  • Arrow functions offer concise syntax and lexical this binding
  • Template literals make string manipulation and HTML generation cleaner
  • Destructuring simplifies extracting values from objects and arrays
  • Classes provide a cleaner syntax for object-oriented programming
  • Modules allow better code organization and dependency management
  • Promises and async/await simplify asynchronous programming
  • Spread and rest operators make working with arrays and objects more powerful
  • WordPress development increasingly relies on modern JavaScript, especially for Gutenberg blocks

Next Steps

In our next session, we'll explore jQuery and see how it compares to modern JavaScript. While jQuery was once essential for cross-browser compatibility, many of its features are now available natively through ES6+ and modern browser APIs. We'll learn when it's appropriate to use jQuery in WordPress development and when native JavaScript is a better choice.

Practice Exercises

Basic Exercises

  1. Convert an existing JavaScript function to use arrow functions, template literals, and destructuring.
  2. Refactor a series of callback functions into an async/await pattern.
  3. Create a simple class to represent a WordPress post with appropriate methods and properties.

Advanced Challenge

Build a simple content filtering system for WordPress posts that:

  1. Fetches posts from the WordPress REST API
  2. Allows filtering by category, tag, and date using ES6+ features
  3. Renders the filtered posts with template literals
  4. Uses Promises or async/await for all asynchronous operations