ES6+ Features Overview
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:
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 changedlet- Lunchbox that only exists in one room; contents can be changedconst- Sealed container that only exists in one room; contents cannot be changed after creation
Best Practices
- Use
constby default for most variables - Use
letwhen you need to reassign a variable - Avoid
varin 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
- Multi-line strings without concatenation or escape characters
- Expression interpolation with
${expression}syntax - 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
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
argumentsobject - Event handlers where
thisshould 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
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
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');
});
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:
- 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'); - Consider browser compatibility and provide fallbacks for older browsers
- 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
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 && (
)}
(
)}
/>
// Using template literals in the save function
save: ({ attributes }) => {
const { quote, author, avatarUrl, backgroundColor } = attributes;
return (
{quote}
{avatarUrl && (
)}
{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.
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
- Feature detection - Check if features exist before using them
- Polyfills - Add missing functionality for older browsers
- 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 (
letandconst) provide better scoping control thanvar - Arrow functions offer concise syntax and lexical
thisbinding - 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
Practice Exercises
Basic Exercises
- Convert an existing JavaScript function to use arrow functions, template literals, and destructuring.
- Refactor a series of callback functions into an async/await pattern.
- 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:
- Fetches posts from the WordPress REST API
- Allows filtering by category, tag, and date using ES6+ features
- Renders the filtered posts with template literals
- Uses Promises or async/await for all asynchronous operations