Master modern JavaScript features and ES6+ syntax for building contemporary applications.
Master modern JavaScript features and ES6+ syntax for building contemporary applications.
Learn modern class syntax, inheritance, static methods, private fields, and advanced class features introduced in ES6 and beyond
Content by: Jigar Solanki
JavaScript Developer
ES6 introduced a cleaner, more intuitive syntax for creating objects and implementing inheritance. Classes provide a more familiar syntax for developers coming from other object-oriented languages while maintaining JavaScript's prototype-based inheritance under the hood.
// Class declaration
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, my name is ${this.name} and I'm ${this.age} years old.`;
}
static createAdult(name) {
return new Person(name, 18);
}
}
const person = new Person('John', 30);
console.log(person.greet()); // "Hello, my name is John and I'm 30 years old."
const adult = Person.createAdult('Jane');
console.log(adult.greet()); // "Hello, my name is Jane and I'm 18 years old."
// Class inheritance
class Employee extends Person {
constructor(name, age, position) {
super(name, age);
this.position = position;
}
work() {
return `${this.name} is working as a ${this.position}.`;
}
}
const employee = new Employee('Bob', 25, 'Developer');
console.log(employee.greet()); // Inherited method
console.log(employee.work()); // "Bob is working as a Developer."
// Private fields (ES2022+)
class BankAccount {
#balance = 0; // Private field
constructor(initialBalance = 0) {
this.#balance = initialBalance;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
return `Deposited ${amount}. New balance: ${this.#balance}`;
}
return 'Invalid deposit amount';
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
return `Withdrew ${amount}. New balance: ${this.#balance}`;
}
return 'Insufficient funds';
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
console.log(account.deposit(500)); // "Deposited 500. New balance: 1500"
console.log(account.withdraw(200)); // "Withdrew 200. New balance: 1300"
// console.log(account.#balance); // Error: Private field
// Getter and setter methods
class Circle {
constructor(radius) {
this._radius = radius;
}
get radius() {
return this._radius;
}
set radius(value) {
if (value > 0) {
this._radius = value;
} else {
throw new Error('Radius must be positive');
}
}
get area() {
return Math.PI * this._radius ** 2;
}
get circumference() {
return 2 * Math.PI * this._radius;
}
}
const circle = new Circle(5);
console.log(circle.radius); // 5
console.log(circle.area); // 78.54...
console.log(circle.circumference); // 31.41...
circle.radius = 10;
console.log(circle.area); // 314.15...
// Exercise: Build a Library Management System
class Book {
constructor(title, author, isbn, year) {
this.title = title;
this.author = author;
this.isbn = isbn;
this.year = year;
this.isAvailable = true;
}
borrow() {
if (this.isAvailable) {
this.isAvailable = false;
return `${this.title} has been borrowed.`;
}
return `${this.title} is not available.`;
}
return() {
this.isAvailable = true;
return `${this.title} has been returned.`;
}
getInfo() {
return `${this.title} by ${this.author} (${this.year}) - ISBN: ${this.isbn}`;
}
}
class Library {
constructor() {
this.books = new Map();
this.members = new Map();
}
addBook(book) {
this.books.set(book.isbn, book);
return `Book ${book.title} added to library.`;
}
removeBook(isbn) {
const book = this.books.get(isbn);
if (book) {
this.books.delete(isbn);
return `Book ${book.title} removed from library.`;
}
return 'Book not found.';
}
findBook(query) {
const results = [];
for (const book of this.books.values()) {
if (book.title.toLowerCase().includes(query.toLowerCase()) ||
book.author.toLowerCase().includes(query.toLowerCase())) {
results.push(book);
}
}
return results;
}
getAvailableBooks() {
return Array.from(this.books.values()).filter(book => book.isAvailable);
}
getBorrowedBooks() {
return Array.from(this.books.values()).filter(book => !book.isAvailable);
}
}
// Test the library system
const library = new Library();
const book1 = new Book('The Great Gatsby', 'F. Scott Fitzgerald', '978-0743273565', 1925);
const book2 = new Book('To Kill a Mockingbird', 'Harper Lee', '978-0446310789', 1960);
const book3 = new Book('1984', 'George Orwell', '978-0451524935', 1949);
library.addBook(book1);
library.addBook(book2);
library.addBook(book3);
console.log(book1.borrow()); // "The Great Gatsby has been borrowed."
console.log(library.getAvailableBooks().length); // 2
console.log(library.findBook('Gatsby')); // [Book object]
Test your understanding of this topic:
Master ES6 modules, import/export syntax, dynamic imports, and modern module patterns for building scalable applications
Content by: Dhara Barmeda
JavaScript Developer
ES6 modules provide a standardized way to organize and share code between files. They offer better encapsulation, explicit dependencies, and improved tooling support compared to traditional script tags.
// math.js - Module with named exports
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;
// constants.js - Module with default export
const PI = 3.14159;
const E = 2.71828;
export default { PI, E };
// utils.js - Mixed exports
export const formatCurrency = (amount) => `$${amount.toFixed(2)}`;
export const formatDate = (date) => date.toLocaleDateString();
const helper = {
validateEmail: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
generateId: () => Math.random().toString(36).substr(2, 9)
};
export default helper;
// main.js - Importing modules
import { add, multiply } from './math.js';
import mathConstants from './constants.js';
import helper, { formatCurrency } from './utils.js';
console.log(add(5, 3)); // 8
console.log(multiply(4, 2)); // 8
console.log(mathConstants.PI); // 3.14159
console.log(formatCurrency(123.456)); // "$123.46"
console.log(helper.validateEmail('test@example.com')); // true
// Dynamic imports
async function loadModule(moduleName) {
try {
const module = await import(`./${moduleName}.js`);
return module;
} catch (error) {
console.error(`Failed to load module: ${moduleName}`, error);
return null;
}
}
// Usage
const mathModule = await loadModule('math');
if (mathModule) {
console.log(mathModule.add(10, 5)); // 15
}
// Re-exporting
// index.js - Barrel export
export { add, subtract } from './math.js';
export { formatCurrency } from './utils.js';
export { default as constants } from './constants.js';
// Import from barrel
import { add, formatCurrency, constants } from './index.js';
// Exercise: Build a Modular Calculator
// calculator/operations.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => {
if (b === 0) throw new Error('Division by zero');
return a / b;
};
export const power = (base, exponent) => Math.pow(base, exponent);
export const sqrt = (number) => {
if (number < 0) throw new Error('Cannot calculate square root of negative number');
return Math.sqrt(number);
};
// calculator/validator.js
export const validateNumber = (value) => {
if (typeof value !== 'number' || isNaN(value)) {
throw new Error('Invalid number');
}
return true;
};
export const validateOperands = (a, b) => {
validateNumber(a);
validateNumber(b);
};
// calculator/formatter.js
export const formatResult = (result, precision = 2) => {
return Number(result.toFixed(precision));
};
export const formatExpression = (operation, a, b, result) => {
return `${a} ${operation} ${b} = ${result}`;
};
// calculator/index.js
import * as operations from './operations.js';
import * as validator from './validator.js';
import * as formatter from './formatter.js';
export class Calculator {
constructor() {
this.history = [];
}
calculate(operation, a, b) {
try {
validator.validateOperands(a, b);
let result;
switch (operation) {
case 'add':
result = operations.add(a, b);
break;
case 'subtract':
result = operations.subtract(a, b);
break;
case 'multiply':
result = operations.multiply(a, b);
break;
case 'divide':
result = operations.divide(a, b);
break;
case 'power':
result = operations.power(a, b);
break;
default:
throw new Error(`Unknown operation: ${operation}`);
}
const formattedResult = formatter.formatResult(result);
const expression = formatter.formatExpression(operation, a, b, formattedResult);
this.history.push({
operation,
operands: [a, b],
result: formattedResult,
expression,
timestamp: new Date()
});
return formattedResult;
} catch (error) {
console.error('Calculation error:', error.message);
throw error;
}
}
getHistory() {
return this.history;
}
clearHistory() {
this.history = [];
}
}
export default Calculator;
Test your understanding of this topic:
Master destructuring assignment, spread operators, and rest parameters for cleaner, more expressive JavaScript code
Content by: Jay Kachhadiya
React.js Developer
Destructuring allows you to extract values from objects and arrays into distinct variables using a syntax that mirrors the construction of array and object literals. This feature makes code more readable and concise.
// Array destructuring
const numbers = [1, 2, 3, 4, 5];
const [first, second, third] = numbers;
console.log(first, second, third); // 1 2 3
// Skip elements
const [a, , c, , e] = numbers;
console.log(a, c, e); // 1 3 5
// Rest operator
const [head, ...tail] = numbers;
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
// Default values
const [x = 0, y = 0, z = 0] = [1, 2];
console.log(x, y, z); // 1 2 0
// Object destructuring
const person = {
name: 'John',
age: 30,
city: 'New York',
country: 'USA'
};
const { name, age, city } = person;
console.log(name, age, city); // John 30 New York
// Rename variables
const { name: fullName, age: userAge } = person;
console.log(fullName, userAge); // John 30
// Default values in objects
const { name: userName, age: userAge2, hobby = 'reading' } = person;
console.log(userName, userAge2, hobby); // John 30 reading
// Nested destructuring
const user = {
id: 1,
profile: {
firstName: 'Jane',
lastName: 'Doe',
contact: {
email: 'jane@example.com',
phone: '123-456-7890'
}
}
};
const { profile: { firstName, contact: { email } } } = user;
console.log(firstName, email); // Jane jane@example.com
// Function parameters
function greet({ name, age = 18 }) {
return `Hello ${name}, you are ${age} years old!`;
}
console.log(greet({ name: 'Alice' })); // "Hello Alice, you are 18 years old!"
// Return value destructuring
function getUser() {
return {
id: 1,
name: 'Bob',
email: 'bob@example.com'
};
}
const { id, name: userName3 } = getUser();
console.log(id, userName3); // 1 Bob
// Spread operator
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// Copy arrays
const original = [1, 2, 3];
const copy = [...original];
copy[0] = 10;
console.log(original); // [1, 2, 3]
console.log(copy); // [10, 2, 3]
// Spread with objects
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const merged = { ...obj1, ...obj2 };
console.log(merged); // { a: 1, b: 2, c: 3, d: 4 }
// Override properties
const baseConfig = { theme: 'dark', language: 'en' };
const userConfig = { theme: 'light' };
const finalConfig = { ...baseConfig, ...userConfig };
console.log(finalConfig); // { theme: 'light', language: 'en' }
// Rest in objects
const { name: userName4, ...otherProps } = person;
console.log(otherProps); // { age: 30, city: 'New York', country: 'USA' }
// Exercise: Build a Data Processor
class DataProcessor {
constructor() {
this.data = [];
}
addData(...items) {
this.data.push(...items);
return this;
}
processUsers(users) {
return users.map(({ name, age, email, ...rest }) => ({
displayName: `${name} (${age})`,
contact: email,
metadata: rest
}));
}
extractCoordinates(locations) {
return locations.map(({ lat, lng, name, ...details }) => ({
position: [lat, lng],
locationName: name,
additionalInfo: details
}));
}
mergeConfigs(...configs) {
return configs.reduce((merged, config) => ({
...merged,
...config
}), {});
}
splitArray(array) {
const [first, second, ...rest] = array;
return {
first,
second,
rest,
total: array.length
};
}
extractQueryParams(url) {
const [, queryString] = url.split('?');
if (!queryString) return {};
return queryString.split('&').reduce((params, pair) => {
const [key, value] = pair.split('=');
return { ...params, [key]: decodeURIComponent(value || '') };
}, {});
}
}
// Test the data processor
const processor = new DataProcessor();
// Test user processing
const users = [
{ name: 'John', age: 30, email: 'john@example.com', role: 'admin' },
{ name: 'Jane', age: 25, email: 'jane@example.com', role: 'user' }
];
const processedUsers = processor.processUsers(users);
console.log('Processed users:', processedUsers);
// Test location processing
const locations = [
{ lat: 40.7128, lng: -74.0060, name: 'New York', country: 'USA' },
{ lat: 51.5074, lng: -0.1278, name: 'London', country: 'UK' }
];
const coordinates = processor.extractCoordinates(locations);
console.log('Coordinates:', coordinates);
// Test config merging
const config1 = { theme: 'dark', language: 'en' };
const config2 = { theme: 'light', debug: true };
const config3 = { apiUrl: 'https://api.example.com' };
const mergedConfig = processor.mergeConfigs(config1, config2, config3);
console.log('Merged config:', mergedConfig);
// Test array splitting
const numbers = [1, 2, 3, 4, 5, 6, 7, 8];
const split = processor.splitArray(numbers);
console.log('Split result:', split);
// Test query params extraction
const url = 'https://example.com/search?q=javascript&page=1&sort=name';
const params = processor.extractQueryParams(url);
console.log('Query params:', params);
Test your understanding of this topic:
Learn template literals, tagged templates, string methods, and modern string manipulation techniques
Content by: Sunny Radadiya
.Net Developer
Template literals are string literals that allow embedded expressions and multiline strings. They provide a more powerful and flexible way to work with strings compared to traditional string concatenation.
// Basic template literals
const name = 'John';
const age = 30;
const greeting = `Hello, my name is ${name} and I'm ${age} years old.`;
console.log(greeting); // "Hello, my name is John and I'm 30 years old."
// Multiline strings
const multiline = `
This is a multiline
string that spans
multiple lines
without needing \n
`;
console.log(multiline);
// Expressions in template literals
const a = 5;
const b = 10;
const calculation = `${a} + ${b} = ${a + b}`;
console.log(calculation); // "5 + 10 = 15"
// Conditional expressions
const isLoggedIn = true;
const message = `Welcome ${isLoggedIn ? 'back' : 'guest'}!`;
console.log(message); // "Welcome back!"
// Function calls
function formatCurrency(amount) {
return `$${amount.toFixed(2)}`;
}
const price = 29.99;
const formattedPrice = `The price is ${formatCurrency(price)}`;
console.log(formattedPrice); // "The price is $29.99"
// Tagged templates
function highlight(strings, ...values) {
let result = '';
strings.forEach((string, i) => {
result += string;
if (i < values.length) {
result += `<span class="highlight">${values[i]}</span>`;
}
});
return result;
}
const highlighted = highlight`Hello ${name}, you are ${age} years old!`;
console.log(highlighted); // "Hello <span class="highlight">John</span>, you are <span class="highlight">30</span> years old!"
// SQL query builder
function sql(strings, ...values) {
let query = '';
strings.forEach((string, i) => {
query += string;
if (i < values.length) {
query += `'${values[i]}'`;
}
});
return query;
}
const tableName = 'users';
const columnName = 'name';
const columnValue = 'John';
const sqlQuery = sql`SELECT * FROM ${tableName} WHERE ${columnName} = ${columnValue}`;
console.log(sqlQuery); // "SELECT * FROM 'users' WHERE 'name' = 'John'"
// HTML template builder
function html(strings, ...values) {
return strings.reduce((result, string, i) => {
return result + string + (values[i] || '');
}, '');
}
const title = 'My Page';
const content = 'Welcome to my website!';
const htmlTemplate = html`
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
</head>
<body>
<h1>${title}</h1>
<p>${content}</p>
</body>
</html>
`;
// String methods with template literals
const text = 'Hello World';
const upperText = `${text.toUpperCase()}`;
const lowerText = `${text.toLowerCase()}`;
const reversedText = `${text.split('').reverse().join('')}`;
console.log(upperText); // "HELLO WORLD"
console.log(lowerText); // "hello world"
console.log(reversedText); // "dlroW olleH"
// Nested template literals
const user = {
name: 'Alice',
preferences: {
theme: 'dark',
language: 'en'
}
};
const userInfo = `
User: ${user.name}
Theme: ${user.preferences.theme}
Language: ${user.preferences.language}
Status: ${user.active ? 'Active' : 'Inactive'}
`;
// Template literal with conditional rendering
function renderUserCard(user) {
return `
<div class="user-card">
<h2>${user.name}</h2>
<p>Email: ${user.email}</p>
${user.avatar ? `<img src="${user.avatar}" alt="Avatar">` : ''}
${user.bio ? `<p>${user.bio}</p>` : ''}
<div class="actions">
${user.isAdmin ? '<button class="admin-btn">Admin Panel</button>' : ''}
<button class="edit-btn">Edit Profile</button>
</div>
</div>
`;
}
// Exercise: Build a Template Engine
class TemplateEngine {
constructor() {
this.templates = new Map();
}
register(name, template) {
this.templates.set(name, template);
}
render(templateName, data) {
const template = this.templates.get(templateName);
if (!template) {
throw new Error(`Template '${templateName}' not found`);
}
return template(data);
}
// Email template
createEmailTemplate() {
return (data) => `
<!DOCTYPE html>
<html>
<head>
<title>${data.subject}</title>
</head>
<body>
<h1>${data.subject}</h1>
<p>Dear ${data.recipientName},</p>
<p>${data.message}</p>
${data.attachments ? `
<h3>Attachments:</h3>
<ul>
${data.attachments.map(file => `<li>${file.name} (${file.size})</li>`).join('')}
</ul>
` : ''}
<p>Best regards,<br>${data.senderName}</p>
</body>
</html>
`;
}
// Report template
createReportTemplate() {
return (data) => `
<div class="report">
<h1>${data.title}</h1>
<p>Generated on: ${new Date().toLocaleDateString()}</p>
<div class="summary">
<h2>Summary</h2>
<p>Total Items: ${data.items.length}</p>
<p>Total Value: ${data.totalValue.toFixed(2)}</p>
</div>
<div class="items">
${data.items.map(item => `
<div class="item">
<h3>${item.name}</h3>
<p>Price: ${item.price.toFixed(2)}</p>
<p>Quantity: ${item.quantity}</p>
<p>Total: ${(item.price * item.quantity).toFixed(2)}</p>
</div>
`).join('')}
</div>
</div>
`;
}
// Notification template
createNotificationTemplate() {
return (data) => `
<div class="notification ${data.type}">
<div class="icon">${data.icon || 'š¢'}</div>
<div class="content">
<h3>${data.title}</h3>
<p>${data.message}</p>
${data.action ? `
<button onclick="${data.action.handler}">
${data.action.text}
</button>
` : ''}
</div>
${data.dismissible ? '<button class="close">Ć</button>' : ''}
</div>
`;
}
}
// Test the template engine
const engine = new TemplateEngine();
// Register templates
engine.register('email', engine.createEmailTemplate());
engine.register('report', engine.createReportTemplate());
engine.register('notification', engine.createNotificationTemplate());
// Test email template
const emailData = {
subject: 'Welcome to our platform',
recipientName: 'John Doe',
message: 'Thank you for joining our platform. We're excited to have you on board!',
senderName: 'The Team',
attachments: [
{ name: 'welcome.pdf', size: '2.5MB' },
{ name: 'guide.docx', size: '1.8MB' }
]
};
const emailHtml = engine.render('email', emailData);
console.log('Email template:', emailHtml);
// Test report template
const reportData = {
title: 'Sales Report - Q1 2024',
items: [
{ name: 'Product A', price: 100, quantity: 5 },
{ name: 'Product B', price: 150, quantity: 3 },
{ name: 'Product C', price: 75, quantity: 8 }
],
totalValue: 1000
};
const reportHtml = engine.render('report', reportData);
console.log('Report template:', reportHtml);
// Test notification template
const notificationData = {
type: 'success',
title: 'Operation Completed',
message: 'Your data has been successfully saved.',
icon: 'ā
',
dismissible: true,
action: {
text: 'View Details',
handler: 'viewDetails()'
}
};
const notificationHtml = engine.render('notification', notificationData);
console.log('Notification template:', notificationHtml);
Test your understanding of this topic:
Master enhanced object literals, computed properties, method shorthand, and modern object patterns
Content by: Kriyansh Khunt
MERN Stack Developer
ES6 introduced several enhancements to object literals that make object creation more concise and expressive. These features include method shorthand, computed property names, and property value shorthand.
// Method shorthand
const calculator = {
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
},
multiply(a, b) {
return a * b;
}
};
console.log(calculator.add(5, 3)); // 8
// Property shorthand
const name = 'John';
const age = 30;
const city = 'New York';
const person = { name, age, city };
console.log(person); // { name: 'John', age: 30, city: 'New York' }
// Computed properties
const propertyName = 'dynamicKey';
const obj = {
[propertyName]: 'dynamic value',
['computed' + 'Key']: 'computed value',
['key${1 + 1}']: 'expression value'
};
console.log(obj); // { dynamicKey: 'dynamic value', computedKey: 'computed value', key2: 'expression value' }
// Object.assign()
const target = { a: 1, b: 2 };
const source1 = { b: 3, c: 4 };
const source2 = { d: 5 };
const result = Object.assign(target, source1, source2);
console.log(result); // { a: 1, b: 3, c: 4, d: 5 }
console.log(target); // { a: 1, b: 3, c: 4, d: 5 }
// Object.is() - Strict equality
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(+0, -0)); // false
console.log(Object.is(5, 5)); // true
// Object.entries(), Object.keys(), Object.values()
const user = {
name: 'Alice',
age: 25,
email: 'alice@example.com'
};
console.log(Object.keys(user)); // ['name', 'age', 'email']
console.log(Object.values(user)); // ['Alice', 25, 'alice@example.com']
console.log(Object.entries(user)); // [['name', 'Alice'], ['age', 25], ['email', 'alice@example.com']]
// Object iteration
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}
// Object.fromEntries()
const entries = [['name', 'Bob'], ['age', 30], ['city', 'London']];
const newObj = Object.fromEntries(entries);
console.log(newObj); // { name: 'Bob', age: 30, city: 'London' }
// Spread operator with objects
const baseConfig = { theme: 'dark', language: 'en' };
const userConfig = { theme: 'light' };
const finalConfig = { ...baseConfig, ...userConfig };
console.log(finalConfig); // { theme: 'light', language: 'en' }
// Object destructuring with defaults
const { name: userName = 'Anonymous', age: userAge = 18, role = 'user' } = user;
console.log(userName, userAge, role); // Alice 25 user
// Nested object enhancement
const config = {
api: {
baseUrl: 'https://api.example.com',
timeout: 5000
},
ui: {
theme: 'dark',
language: 'en'
},
getFullConfig() {
return { ...this.api, ...this.ui };
}
};
console.log(config.getFullConfig());
// Object with getters and setters
const circle = {
radius: 5,
get diameter() {
return this.radius * 2;
},
set diameter(value) {
this.radius = value / 2;
},
get area() {
return Math.PI * this.radius ** 2;
}
};
console.log(circle.diameter); // 10
circle.diameter = 20;
console.log(circle.radius); // 10
console.log(circle.area); // 314.15...
// Exercise: Build a Configuration Manager
class ConfigManager {
constructor() {
this.config = {};
this.defaults = {};
this.validators = new Map();
}
setDefault(key, value) {
this.defaults[key] = value;
return this;
}
setValidator(key, validator) {
this.validators.set(key, validator);
return this;
}
set(key, value) {
const validator = this.validators.get(key);
if (validator && !validator(value)) {
throw new Error(`Invalid value for ${key}: ${value}`);
}
this.config[key] = value;
return this;
}
get(key) {
return this.config[key] ?? this.defaults[key];
}
has(key) {
return key in this.config || key in this.defaults;
}
merge(config) {
this.config = { ...this.config, ...config };
return this;
}
reset() {
this.config = {};
return this;
}
toObject() {
return { ...this.defaults, ...this.config };
}
fromObject(obj) {
this.config = { ...obj };
return this;
}
validate() {
const errors = [];
for (const [key, validator] of this.validators) {
const value = this.get(key);
if (!validator(value)) {
errors.push(`Invalid value for ${key}: ${value}`);
}
}
return errors;
}
}
// Exercise: Build a State Manager with Enhanced Objects
class StateManager {
constructor(initialState = {}) {
this.state = { ...initialState };
this.listeners = new Map();
this.history = [];
}
setState(updates) {
const oldState = { ...this.state };
this.state = { ...this.state, ...updates };
this.history.push({
timestamp: new Date(),
oldState,
newState: { ...this.state },
updates
});
this.notifyListeners(updates, oldState);
return this;
}
getState() {
return { ...this.state };
}
subscribe(key, listener) {
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
}
this.listeners.get(key).add(listener);
return () => {
const keyListeners = this.listeners.get(key);
if (keyListeners) {
keyListeners.delete(listener);
}
};
}
notifyListeners(updates, oldState) {
for (const [key, value] of Object.entries(updates)) {
const listeners = this.listeners.get(key);
if (listeners) {
listeners.forEach(listener => {
try {
listener(value, oldState[key], this.state);
} catch (error) {
console.error('Listener error:', error);
}
});
}
}
}
getHistory() {
return [...this.history];
}
undo() {
if (this.history.length > 0) {
const lastChange = this.history.pop();
this.state = { ...lastChange.oldState };
this.notifyListeners(lastChange.updates, lastChange.newState);
}
}
}
// Test the configuration manager
const configManager = new ConfigManager()
.setDefault('theme', 'dark')
.setDefault('language', 'en')
.setDefault('timeout', 5000)
.setValidator('theme', value => ['light', 'dark'].includes(value))
.setValidator('timeout', value => typeof value === 'number' && value > 0);
configManager.set('theme', 'light');
configManager.set('timeout', 3000);
console.log('Config:', configManager.toObject());
console.log('Validation errors:', configManager.validate());
// Test the state manager
const stateManager = new StateManager({
user: null,
theme: 'dark',
notifications: []
});
const unsubscribe = stateManager.subscribe('user', (newUser, oldUser) => {
console.log('User changed:', { old: oldUser, new: newUser });
});
stateManager.setState({
user: { name: 'John', id: 1 },
theme: 'light'
});
stateManager.setState({
notifications: ['Welcome!', 'New message']
});
console.log('Current state:', stateManager.getState());
console.log('History length:', stateManager.getHistory().length);
stateManager.undo();
console.log('After undo:', stateManager.getState());
Test your understanding of this topic:
Continue your learning journey and master the next set of concepts.
Continue to Module 6