JavaScript 2024 (ES15)
Promise.withResolvers, Object.groupBy, and modern enhancements
π What's New in JavaScript 2024?
JavaScript 2024 (ES15) brings exciting features like Promise.withResolvers(), Object.groupBy(), and enhanced regular expressions. These additions make JavaScript more powerful for modern web development.
// New in 2024: Promise.withResolvers()
const { promise, resolve, reject } = Promise.withResolvers();
// Use resolve/reject from outside the promise
setTimeout(() => resolve('Success!'), 1000);
const result = await promise;
console.log(result); // 'Success!'
Key Features of ES15
Promise.withResolvers()
Create promises with external resolvers
const { promise, resolve } =
Promise.withResolvers();
resolve('Done!');
Object.groupBy()
Group array elements by key
const grouped = Object.groupBy(
users, user => user.role
);
String.isWellFormed()
Check if string is well-formed Unicode
const text = "Hello π";
console.log(text.isWellFormed());
// true
ArrayBuffer.resize()
Resize ArrayBuffers dynamically
const buffer = new ArrayBuffer(8);
buffer.resize(16);
console.log(buffer.byteLength);
πΉ Promise.withResolvers()
Create promises with external control over resolution:
// Traditional promise creation
const traditionalPromise = new Promise((resolve, reject) => {
// Logic must be inside here
setTimeout(() => resolve('Traditional way'), 1000);
});
// New way with withResolvers()
const { promise, resolve, reject } = Promise.withResolvers();
// Can resolve from anywhere in your code!
setTimeout(() => resolve('Modern way'), 1000);
// Practical example: Event-driven promise resolution
class EventManager {
constructor() {
this.listeners = new Map();
}
waitForEvent(eventName) {
const { promise, resolve } = Promise.withResolvers();
// Store resolver for later use
if (!this.listeners.has(eventName)) {
this.listeners.set(eventName, []);
}
this.listeners.get(eventName).push(resolve);
return promise;
}
emit(eventName, data) {
const resolvers = this.listeners.get(eventName) || [];
resolvers.forEach(resolve => resolve(data));
this.listeners.delete(eventName); // Clean up
}
}
// Usage
const eventManager = new EventManager();
// Wait for user login from anywhere in your app
const loginPromise = eventManager.waitForEvent('userLogin');
console.log('Waiting for user login...');
// Simulate login happening elsewhere
setTimeout(() => {
eventManager.emit('userLogin', { userId: 123, name: 'Alice' });
}, 2000);
loginPromise.then(user => {
console.log('User logged in:', user.name);
});
Output:
Waiting for user login... User logged in: Alice
πΉ Object.groupBy() and Map.groupBy()
Group array elements by a key function:
const employees = [
{ name: 'Alice', department: 'Engineering', salary: 90000 },
{ name: 'Bob', department: 'Marketing', salary: 65000 },
{ name: 'Charlie', department: 'Engineering', salary: 95000 },
{ name: 'Diana', department: 'Sales', salary: 70000 },
{ name: 'Eve', department: 'Marketing', salary: 68000 },
{ name: 'Frank', department: 'Engineering', salary: 88000 }
];
// Group by department using Object.groupBy()
const byDepartment = Object.groupBy(employees, emp => emp.department);
console.log('Grouped by department:', byDepartment);
// Group by salary range using Map.groupBy()
const bySalaryRange = Map.groupBy(employees, emp => {
if (emp.salary < 70000) return 'Low';
if (emp.salary < 90000) return 'Medium';
return 'High';
});
console.log('High earners:', bySalaryRange.get('High'));
// Advanced grouping: Multiple criteria
const products = [
{ name: 'Laptop', category: 'Electronics', price: 999, inStock: true },
{ name: 'Phone', category: 'Electronics', price: 699, inStock: false },
{ name: 'Desk', category: 'Furniture', price: 299, inStock: true },
{ name: 'Chair', category: 'Furniture', price: 199, inStock: true },
{ name: 'Tablet', category: 'Electronics', price: 399, inStock: true }
];
// Group by availability and category
const groupedProducts = Object.groupBy(products, product =>
`${product.category}-${product.inStock ? 'Available' : 'OutOfStock'}`
);
console.log('Product groups:', Object.keys(groupedProducts));
console.log('Available Electronics:', groupedProducts['Electronics-Available']);
Output:
Grouped by department: {
Engineering: [Alice, Charlie, Frank],
Marketing: [Bob, Eve],
Sales: [Diana]
}
High earners: [Alice, Charlie]
Product groups: ['Electronics-Available', 'Electronics-OutOfStock', 'Furniture-Available']
Available Electronics: [Laptop, Tablet]
πΉ String Unicode Methods
Better handling of Unicode strings:
// String.isWellFormed() - check for valid Unicode
const validString = "Hello π World π";
const invalidString = "Hello\uD800World"; // Lone surrogate
console.log('Valid string:', validString.isWellFormed()); // true
console.log('Invalid string:', invalidString.isWellFormed()); // false
// String.toWellFormed() - fix malformed Unicode
const fixedString = invalidString.toWellFormed();
console.log('Fixed string:', fixedString); // "HelloοΏ½World" (replacement character)
// Practical example: Safe string processing
function safeStringProcess(input) {
if (!input.isWellFormed()) {
console.warn('Malformed Unicode detected, fixing...');
input = input.toWellFormed();
}
return input.toUpperCase();
}
console.log(safeStringProcess("Hello π")); // "HELLO π"
console.log(safeStringProcess("Bad\uD800String")); // "BADοΏ½STRING"
// Working with emojis and complex Unicode
const emojiString = "π¨βπ©βπ§βπ¦ Family emoji";
console.log('Emoji string well-formed:', emojiString.isWellFormed()); // true
console.log('Length:', emojiString.length); // 16 (complex emoji counts as multiple units)
// Proper emoji handling
const emojis = ['π', 'π', 'π', 'π»', 'π'];
const emojiText = emojis.join(' ');
console.log('Emoji text:', emojiText);
console.log('Is well-formed:', emojiText.isWellFormed()); // true
Output:
Valid string: true Invalid string: false Fixed string: HelloοΏ½World HELLO π BADοΏ½STRING Emoji string well-formed: true Length: 16 Emoji text: π π π π» π Is well-formed: true
πΉ ArrayBuffer Enhancements
Dynamic resizing and transfer of ArrayBuffers:
// Resizable ArrayBuffer
const resizableBuffer = new ArrayBuffer(8, { maxByteLength: 64 });
console.log('Initial size:', resizableBuffer.byteLength); // 8
console.log('Max size:', resizableBuffer.maxByteLength); // 64
console.log('Resizable:', resizableBuffer.resizable); // true
// Resize the buffer
resizableBuffer.resize(16);
console.log('After resize:', resizableBuffer.byteLength); // 16
// Working with views on resizable buffers
const uint8View = new Uint8Array(resizableBuffer);
uint8View[0] = 42;
uint8View[8] = 100; // Now accessible after resize
console.log('Data at index 0:', uint8View[0]); // 42
console.log('Data at index 8:', uint8View[8]); // 100
// Transferable ArrayBuffer
const transferableBuffer = new ArrayBuffer(16);
const originalView = new Uint8Array(transferableBuffer);
originalView[0] = 123;
console.log('Before transfer:', originalView[0]); // 123
// Transfer ownership (simulated - actual transfer happens between workers)
const transferredBuffer = transferableBuffer.transfer();
console.log('Original buffer detached:', transferableBuffer.byteLength); // 0
console.log('Transferred buffer size:', transferredBuffer.byteLength); // 16
// Practical example: Dynamic buffer for streaming data
class StreamBuffer {
constructor(initialSize = 1024) {
this.buffer = new ArrayBuffer(initialSize, { maxByteLength: 1024 * 1024 });
this.view = new Uint8Array(this.buffer);
this.position = 0;
}
write(data) {
// Resize if needed
if (this.position + data.length > this.buffer.byteLength) {
const newSize = Math.min(
Math.max(this.buffer.byteLength * 2, this.position + data.length),
this.buffer.maxByteLength
);
this.buffer.resize(newSize);
this.view = new Uint8Array(this.buffer);
}
// Write data
this.view.set(data, this.position);
this.position += data.length;
}
getData() {
return this.view.slice(0, this.position);
}
}
// Usage
const stream = new StreamBuffer(8);
stream.write(new Uint8Array([1, 2, 3, 4]));
stream.write(new Uint8Array([5, 6, 7, 8, 9, 10])); // Triggers resize
console.log('Stream data:', Array.from(stream.getData())); // [1,2,3,4,5,6,7,8,9,10]
Output:
Initial size: 8 Max size: 64 Resizable: true After resize: 16 Data at index 0: 42 Data at index 8: 100 Before transfer: 123 Original buffer detached: 0 Transferred buffer size: 16 Stream data: [1,2,3,4,5,6,7,8,9,10]
πΉ Real-World Applications
Combining ES15 features for practical solutions:
// Advanced data processing pipeline
class DataProcessor {
constructor() {
this.cache = new Map();
this.{ promise: processingComplete, resolve: markComplete } = Promise.withResolvers();
}
async processUserData(users) {
// Group users by role using Object.groupBy()
const usersByRole = Object.groupBy(users, user => user.role);
// Process each group
const results = {};
for (const [role, roleUsers] of Object.entries(usersByRole)) {
results[role] = await this.processRole(roleUsers);
}
// Mark processing as complete
this.markComplete(results);
return results;
}
async processRole(users) {
// Simulate processing with proper Unicode handling
return users.map(user => ({
...user,
displayName: user.name.isWellFormed() ?
user.name : user.name.toWellFormed(),
processed: true
}));
}
async waitForCompletion() {
return this.processingComplete;
}
}
// Usage example
const processor = new DataProcessor();
const userData = [
{ id: 1, name: 'Alice π©βπ»', role: 'developer' },
{ id: 2, name: 'Bob π¨βπΌ', role: 'manager' },
{ id: 3, name: 'Charlie π¨βπ»', role: 'developer' },
{ id: 4, name: 'Diana π©βπΌ', role: 'manager' }
];
// Process data and wait for completion
processor.processUserData(userData);
const results = await processor.waitForCompletion();
console.log('Processing complete:', Object.keys(results));
Output:
Processing complete: ['developer', 'manager']