Master ES6 class syntax, constructor functions, inheritance with extends, and super keyword usage.
ES6 classes provide a cleaner, more intuitive syntax for creating objects and implementing inheritance in JavaScript. While JavaScript remains prototype-based under the hood, classes offer a familiar object-oriented programming interface that makes code more readable and maintainable.
Beyond basic class definitions, ES6 classes support inheritance, static methods, getters/setters, and modern features like private fields. They enable elegant design patterns including mixins, abstract classes, and factory methods that are essential for large-scale application development.
In this lesson, you'll explore class syntax, inheritance hierarchies, advanced class features, and learn to build robust object-oriented systems using modern JavaScript class patterns that are widely used in frameworks like React, Vue, and Node.js applications.
ES6 classes provide a clean syntax for creating constructor functions with methods, replacing traditional prototype-based patterns with more readable code.
1. Basic Class Declaration
1// Traditional constructor function
2function PersonOld(name, age) {
3 this.name = name;
4 this.age = age;
5}
6
7PersonOld.prototype.greet = function() {
8 return `Hello, I'm ${this.name}`;
9};
10
11PersonOld.prototype.getAge = function() {
12 return this.age;
13};
14
15// ES6 Class syntax
16class Person {
17 constructor(name, age) {
18 this.name = name;
19 this.age = age;
20 }
21
22 greet() {
23 return `Hello, I'm ${this.name}`;
24 }
25
26 getAge() {
27 return this.age;
28 }
29
30 // Getter method
31 get displayName() {
32 return `${this.name} (Age: ${this.age})`;
33 }
34
35 // Setter method
36 set age(value) {
37 if (value < 0) {
38 throw new Error('Age cannot be negative');
39 }
40 this._age = value;
41 }
42
43 get age() {
44 return this._age;
45 }
46
47 // Static method
48 static createAdult(name) {
49 return new Person(name, 18);
50 }
51}
52
53// Usage
54const person = new Person('Alice', 30);
55console.log(person.greet()); // "Hello, I'm Alice"
56console.log(person.displayName); // "Alice (Age: 30)"
57
58const adult = Person.createAdult('Bob');
59console.log(adult.getAge()); // 182. Advanced Constructor Patterns
1class BankAccount {
2 constructor(accountNumber, initialBalance = 0, accountType = 'savings') {
3 // Public properties
4 this.accountNumber = accountNumber;
5 this.accountType = accountType;
6 this.createdAt = new Date();
7
8 // Private-like properties (convention)
9 this._balance = initialBalance;
10 this._transactions = [];
11
12 // Validation
13 this._validateAccountNumber(accountNumber);
14 this._validateBalance(initialBalance);
15 }
16
17 // Private methods (convention with underscore)
18 _validateAccountNumber(number) {
19 if (!number || typeof number !== 'string') {
20 throw new Error('Valid account number is required');
21 }
22 }
23
24 _validateBalance(balance) {
25 if (balance < 0) {
26 throw new Error('Initial balance cannot be negative');
27 }
28 }
29
30 _addTransaction(type, amount, description) {
31 this._transactions.push({
32 type,
33 amount,
34 description,
35 timestamp: new Date(),
36 balance: this._balance
37 });
38 }
39
40 // Public methods
41 deposit(amount) {
42 if (amount <= 0) {
43 throw new Error('Deposit amount must be positive');
44 }
45
46 this._balance += amount;
47 this._addTransaction('deposit', amount, `Deposit of $${amount}`);
48 return this._balance;
49 }
50
51 withdraw(amount) {
52 if (amount <= 0) {
53 throw new Error('Withdrawal amount must be positive');
54 }
55
56 if (amount > this._balance) {
57 throw new Error('Insufficient funds');
58 }
59
60 this._balance -= amount;
61 this._addTransaction('withdrawal', -amount, `Withdrawal of $${amount}`);
62 return this._balance;
63 }
64
65 // Getter for balance
66 get balance() {
67 return this._balance;
68 }
69
70 // Getter for transaction history
71 get transactionHistory() {
72 return [...this._transactions]; // Return copy
73 }
74
75 // Method to get account summary
76 getAccountSummary() {
77 return {
78 accountNumber: this.accountNumber,
79 accountType: this.accountType,
80 balance: this._balance,
81 transactionCount: this._transactions.length,
82 createdAt: this.createdAt
83 };
84 }
85
86 // Static method to validate account number format
87 static isValidAccountNumber(number) {
88 return /^\d{10,12}$/.test(number);
89 }
90}
91
92// Usage
93const account = new BankAccount('1234567890', 1000);
94account.deposit(500);
95account.withdraw(200);
96console.log(account.balance); // 1300
97console.log(account.getAccountSummary());3. Class Expressions and Dynamic Classes
1// Named class expression
2const User = class UserClass {
3 constructor(name) {
4 this.name = name;
5 }
6
7 toString() {
8 return `User: ${this.name}`;
9 }
10};
11
12// Anonymous class expression
13const createModelClass = (modelName) => {
14 return class {
15 constructor(data) {
16 this.modelName = modelName;
17 this.data = data;
18 this.id = Math.random().toString(36).substr(2, 9);
19 }
20
21 save() {
22 console.log(`Saving ${this.modelName} with ID: ${this.id}`);
23 return this;
24 }
25
26 static find(id) {
27 console.log(`Finding ${modelName} with ID: ${id}`);
28 return new this({ id });
29 }
30 };
31};
32
33// Dynamic class creation
34const Product = createModelClass('Product');
35const Category = createModelClass('Category');
36
37const product = new Product({ name: 'Laptop', price: 999 });
38product.save();ES6 provides extends and super keywords for creating inheritance hierarchies, enabling code reuse and polymorphism in object-oriented designs.
1. Basic Inheritance
1// Base class
2class Animal {
3 constructor(name, species) {
4 this.name = name;
5 this.species = species;
6 this.isAlive = true;
7 }
8
9 speak() {
10 return `${this.name} makes a sound`;
11 }
12
13 move() {
14 return `${this.name} moves`;
15 }
16
17 getInfo() {
18 return {
19 name: this.name,
20 species: this.species,
21 isAlive: this.isAlive
22 };
23 }
24
25 static getKingdom() {
26 return 'Animalia';
27 }
28}
29
30// Derived class
31class Dog extends Animal {
32 constructor(name, breed) {
33 super(name, 'Canine'); // Call parent constructor
34 this.breed = breed;
35 this.tricks = [];
36 }
37
38 // Override parent method
39 speak() {
40 return `${super.speak()} - Woof!`;
41 }
42
43 // Override parent method
44 move() {
45 return `${super.move()} by running on four legs`;
46 }
47
48 // New methods specific to Dog
49 learnTrick(trick) {
50 this.tricks.push(trick);
51 return `${this.name} learned to ${trick}!`;
52 }
53
54 performTrick(trick) {
55 if (this.tricks.includes(trick)) {
56 return `${this.name} performs ${trick}!`;
57 }
58 return `${this.name} doesn't know how to ${trick}`;
59 }
60
61 // Override getInfo to include breed
62 getInfo() {
63 return {
64 ...super.getInfo(),
65 breed: this.breed,
66 tricks: [...this.tricks]
67 };
68 }
69}
70
71// Further inheritance
72class ServiceDog extends Dog {
73 constructor(name, breed, serviceType) {
74 super(name, breed);
75 this.serviceType = serviceType;
76 this.isWorking = false;
77 this.certifications = [];
78 }
79
80 startWork() {
81 this.isWorking = true;
82 return `${this.name} is now working as a ${this.serviceType} dog`;
83 }
84
85 stopWork() {
86 this.isWorking = false;
87 return `${this.name} is off duty`;
88 }
89
90 addCertification(cert) {
91 this.certifications.push({
92 name: cert,
93 dateEarned: new Date()
94 });
95 return `${this.name} earned ${cert} certification`;
96 }
97
98 getInfo() {
99 return {
100 ...super.getInfo(),
101 serviceType: this.serviceType,
102 isWorking: this.isWorking,
103 certifications: [...this.certifications]
104 };
105 }
106}
107
108// Usage
109const buddy = new ServiceDog('Buddy', 'Golden Retriever', 'Guide');
110console.log(buddy.speak()); // "Buddy makes a sound - Woof!"
111console.log(buddy.learnTrick('sit'));
112console.log(buddy.addCertification('Guide Dog Training'));
113console.log(buddy.startWork());
114console.log(buddy.getInfo());2. Method Chaining and Fluent Interfaces
1class QueryBuilder {
2 constructor() {
3 this.query = {
4 select: [],
5 from: '',
6 where: [],
7 orderBy: [],
8 limit: null
9 };
10 }
11
12 select(...fields) {
13 this.query.select.push(...fields);
14 return this; // Return this for chaining
15 }
16
17 from(table) {
18 this.query.from = table;
19 return this;
20 }
21
22 where(condition) {
23 this.query.where.push(condition);
24 return this;
25 }
26
27 orderBy(field, direction = 'ASC') {
28 this.query.orderBy.push(`${field} ${direction}`);
29 return this;
30 }
31
32 limit(count) {
33 this.query.limit = count;
34 return this;
35 }
36
37 build() {
38 const parts = [];
39
40 if (this.query.select.length > 0) {
41 parts.push(`SELECT ${this.query.select.join(', ')}`);
42 }
43
44 if (this.query.from) {
45 parts.push(`FROM ${this.query.from}`);
46 }
47
48 if (this.query.where.length > 0) {
49 parts.push(`WHERE ${this.query.where.join(' AND ')}`);
50 }
51
52 if (this.query.orderBy.length > 0) {
53 parts.push(`ORDER BY ${this.query.orderBy.join(', ')}`);
54 }
55
56 if (this.query.limit) {
57 parts.push(`LIMIT ${this.query.limit}`);
58 }
59
60 return parts.join(' ');
61 }
62
63 clone() {
64 const cloned = new QueryBuilder();
65 cloned.query = JSON.parse(JSON.stringify(this.query));
66 return cloned;
67 }
68}
69
70// Extended query builder
71class AdvancedQueryBuilder extends QueryBuilder {
72 constructor() {
73 super();
74 this.query.joins = [];
75 this.query.groupBy = [];
76 this.query.having = [];
77 }
78
79 join(table, condition, type = 'INNER') {
80 this.query.joins.push(`${type} JOIN ${table} ON ${condition}`);
81 return this;
82 }
83
84 leftJoin(table, condition) {
85 return this.join(table, condition, 'LEFT');
86 }
87
88 groupBy(...fields) {
89 this.query.groupBy.push(...fields);
90 return this;
91 }
92
93 having(condition) {
94 this.query.having.push(condition);
95 return this;
96 }
97
98 build() {
99 let sql = super.build();
100
101 if (this.query.joins.length > 0) {
102 const parts = sql.split(' WHERE ');
103 const beforeWhere = parts[0];
104 const afterWhere = parts[1] || '';
105
106 sql = beforeWhere + ' ' + this.query.joins.join(' ');
107 if (afterWhere) {
108 sql += ' WHERE ' + afterWhere;
109 }
110 }
111
112 if (this.query.groupBy.length > 0) {
113 sql += ` GROUP BY ${this.query.groupBy.join(', ')}`;
114 }
115
116 if (this.query.having.length > 0) {
117 sql += ` HAVING ${this.query.having.join(' AND ')}`;
118 }
119
120 return sql;
121 }
122}
123
124// Usage
125const query = new AdvancedQueryBuilder()
126 .select('u.name', 'u.email', 'COUNT(o.id) as order_count')
127 .from('users u')
128 .leftJoin('orders o', 'o.user_id = u.id')
129 .where('u.active = 1')
130 .groupBy('u.id')
131 .having('COUNT(o.id) > 0')
132 .orderBy('order_count', 'DESC')
133 .limit(10)
134 .build();
135
136console.log(query);Modern JavaScript classes support static methods for utility functions and private fields for true encapsulation using # syntax.
1. Static Methods and Properties
1class MathUtils {
2 // Static properties
3 static PI = 3.14159;
4 static E = 2.71828;
5
6 // Static methods
7 static add(a, b) {
8 return a + b;
9 }
10
11 static multiply(a, b) {
12 return a * b;
13 }
14
15 static factorial(n) {
16 if (n <= 1) return 1;
17 return n * MathUtils.factorial(n - 1);
18 }
19
20 static randomInRange(min, max) {
21 return Math.random() * (max - min) + min;
22 }
23
24 // Static method with validation
25 static distance(point1, point2) {
26 if (!point1 || !point2) {
27 throw new Error('Both points are required');
28 }
29
30 const dx = point2.x - point1.x;
31 const dy = point2.y - point1.y;
32 return Math.sqrt(dx * dx + dy * dy);
33 }
34
35 // Static factory method
36 static createCircle(radius) {
37 return {
38 radius,
39 area: MathUtils.PI * radius * radius,
40 circumference: 2 * MathUtils.PI * radius
41 };
42 }
43}
44
45// Usage - no need to instantiate
46console.log(MathUtils.add(5, 3)); // 8
47console.log(MathUtils.factorial(5)); // 120
48console.log(MathUtils.distance({x: 0, y: 0}, {x: 3, y: 4})); // 5
49
50const circle = MathUtils.createCircle(10);
51console.log(circle); // { radius: 10, area: 314.159, circumference: 62.832 }2. Private Fields (Modern JavaScript)
1class BankAccountSecure {
2 // Private fields
3 #balance;
4 #accountNumber;
5 #transactions;
6
7 // Private static field
8 static #bankCode = 'BANK001';
9
10 constructor(accountNumber, initialBalance = 0) {
11 this.#accountNumber = accountNumber;
12 this.#balance = initialBalance;
13 this.#transactions = [];
14 this.accountType = 'checking'; // Public field
15 this.createdAt = new Date();
16 }
17
18 // Private method
19 #validateAmount(amount) {
20 if (typeof amount !== 'number' || amount <= 0) {
21 throw new Error('Amount must be a positive number');
22 }
23 }
24
25 #recordTransaction(type, amount, description) {
26 this.#transactions.push({
27 id: Math.random().toString(36).substr(2, 9),
28 type,
29 amount,
30 description,
31 timestamp: new Date(),
32 balanceAfter: this.#balance
33 });
34 }
35
36 // Public methods
37 deposit(amount) {
38 this.#validateAmount(amount);
39 this.#balance += amount;
40 this.#recordTransaction('deposit', amount, `Deposit of $${amount}`);
41 return this.#balance;
42 }
43
44 withdraw(amount) {
45 this.#validateAmount(amount);
46
47 if (amount > this.#balance) {
48 throw new Error('Insufficient funds');
49 }
50
51 this.#balance -= amount;
52 this.#recordTransaction('withdrawal', -amount, `Withdrawal of $${amount}`);
53 return this.#balance;
54 }
55
56 // Getter for balance (read-only access to private field)
57 get balance() {
58 return this.#balance;
59 }
60
61 // Getter for account number (read-only)
62 get accountNumber() {
63 return this.#accountNumber;
64 }
65
66 // Get transaction history (returns copy)
67 getTransactionHistory(limit = 10) {
68 return this.#transactions
69 .slice(-limit)
70 .map(transaction => ({ ...transaction }));
71 }
72
73 // Static method using private static field
74 static getBankCode() {
75 return BankAccountSecure.#bankCode;
76 }
77
78 // Static method to create account with validation
79 static createAccount(accountNumber, initialDeposit = 0) {
80 if (!BankAccountSecure.isValidAccountNumber(accountNumber)) {
81 throw new Error('Invalid account number format');
82 }
83
84 return new BankAccountSecure(accountNumber, initialDeposit);
85 }
86
87 static isValidAccountNumber(number) {
88 return /^\d{10,12}$/.test(number);
89 }
90}
91
92// Usage
93const account = BankAccountSecure.createAccount('1234567890', 1000);
94console.log(account.balance); // 1000
95account.deposit(500);
96
97// These would throw errors (private fields not accessible):
98// console.log(account.#balance); // SyntaxError
99// account.#recordTransaction('fake', 100); // SyntaxError
100
101console.log(account.getTransactionHistory());
102console.log(BankAccountSecure.getBankCode());3. Mixins and Composition Patterns
1// Mixin factory function
2const Timestamped = (Base) => class extends Base {
3 constructor(...args) {
4 super(...args);
5 this.createdAt = new Date();
6 this.updatedAt = new Date();
7 }
8
9 touch() {
10 this.updatedAt = new Date();
11 return this;
12 }
13
14 getAge() {
15 return Date.now() - this.createdAt.getTime();
16 }
17};
18
19// Another mixin
20const Identifiable = (Base) => class extends Base {
21 constructor(...args) {
22 super(...args);
23 this.id = Math.random().toString(36).substr(2, 9);
24 }
25
26 equals(other) {
27 return this.id === other?.id;
28 }
29
30 toString() {
31 return `${this.constructor.name}(${this.id})`;
32 }
33};
34
35// Validation mixin
36const Validatable = (Base) => class extends Base {
37 constructor(...args) {
38 super(...args);
39 this.errors = [];
40 }
41
42 validate() {
43 this.errors = [];
44 this._runValidations();
45 return this.errors.length === 0;
46 }
47
48 _runValidations() {
49 // Override in subclasses
50 }
51
52 addError(field, message) {
53 this.errors.push({ field, message });
54 }
55
56 isValid() {
57 return this.validate();
58 }
59
60 getErrors() {
61 return [...this.errors];
62 }
63};
64
65// Base class
66class User {
67 constructor(name, email) {
68 this.name = name;
69 this.email = email;
70 }
71
72 greet() {
73 return `Hello, I'm ${this.name}`;
74 }
75}
76
77// Apply multiple mixins
78class EnhancedUser extends Validatable(Timestamped(Identifiable(User))) {
79 constructor(name, email) {
80 super(name, email);
81 }
82
83 _runValidations() {
84 if (!this.name || this.name.length < 2) {
85 this.addError('name', 'Name must be at least 2 characters');
86 }
87
88 if (!this.email || !this.email.includes('@')) {
89 this.addError('email', 'Valid email is required');
90 }
91 }
92
93 updateProfile(name, email) {
94 this.name = name;
95 this.email = email;
96 this.touch();
97 return this;
98 }
99}
100
101// Usage
102const user = new EnhancedUser('Alice Johnson', '[email protected]');
103console.log(user.toString()); // EnhancedUser(randomId)
104console.log(user.isValid()); // true
105console.log(user.getAge()); // milliseconds since creation
106
107user.updateProfile('Alice Smith', '[email protected]');
108console.log(user.updatedAt); // Updated timestampModern class designs include abstract classes, factory patterns, decorators, and advanced architectural patterns for robust application development.
1. Abstract Base Classes Pattern
1// Abstract base class pattern
2class Shape {
3 constructor(name) {
4 if (new.target === Shape) {
5 throw new Error('Shape is abstract and cannot be instantiated directly');
6 }
7 this.name = name;
8 }
9
10 // Abstract methods (must be overridden)
11 getArea() {
12 throw new Error('getArea() must be implemented by subclass');
13 }
14
15 getPerimeter() {
16 throw new Error('getPerimeter() must be implemented by subclass');
17 }
18
19 // Concrete method
20 describe() {
21 return `${this.name}: Area = ${this.getArea()}, Perimeter = ${this.getPerimeter()}`;
22 }
23
24 // Template method pattern
25 draw() {
26 this.setupDrawing();
27 this.renderShape();
28 this.finishDrawing();
29 }
30
31 setupDrawing() {
32 console.log(`Setting up to draw ${this.name}`);
33 }
34
35 renderShape() {
36 throw new Error('renderShape() must be implemented by subclass');
37 }
38
39 finishDrawing() {
40 console.log(`Finished drawing ${this.name}`);
41 }
42
43 static isShape(obj) {
44 return obj instanceof Shape;
45 }
46}
47
48class Rectangle extends Shape {
49 constructor(width, height) {
50 super('Rectangle');
51 this.width = width;
52 this.height = height;
53 }
54
55 getArea() {
56 return this.width * this.height;
57 }
58
59 getPerimeter() {
60 return 2 * (this.width + this.height);
61 }
62
63 renderShape() {
64 console.log(`Rendering rectangle: ${this.width} x ${this.height}`);
65 }
66}
67
68class Circle extends Shape {
69 constructor(radius) {
70 super('Circle');
71 this.radius = radius;
72 }
73
74 getArea() {
75 return Math.PI * this.radius * this.radius;
76 }
77
78 getPerimeter() {
79 return 2 * Math.PI * this.radius;
80 }
81
82 renderShape() {
83 console.log(`Rendering circle with radius: ${this.radius}`);
84 }
85}
86
87// Usage
88const rectangle = new Rectangle(5, 3);
89const circle = new Circle(4);
90
91console.log(rectangle.describe());
92console.log(circle.describe());
93
94rectangle.draw();
95circle.draw();
96
97// This would throw an error:
98// const shape = new Shape('Generic'); // Error: Shape is abstract2. Factory Pattern with Classes
1// Factory base class
2class DatabaseConnection {
3 constructor(config) {
4 this.config = config;
5 this.isConnected = false;
6 }
7
8 connect() {
9 throw new Error('connect() must be implemented by subclass');
10 }
11
12 disconnect() {
13 throw new Error('disconnect() must be implemented by subclass');
14 }
15
16 query(sql) {
17 throw new Error('query() must be implemented by subclass');
18 }
19}
20
21class MySQLConnection extends DatabaseConnection {
22 constructor(config) {
23 super(config);
24 this.type = 'MySQL';
25 }
26
27 connect() {
28 console.log(`Connecting to MySQL: ${this.config.host}:${this.config.port}`);
29 this.isConnected = true;
30 return this;
31 }
32
33 disconnect() {
34 console.log('Disconnecting from MySQL');
35 this.isConnected = false;
36 return this;
37 }
38
39 query(sql) {
40 if (!this.isConnected) {
41 throw new Error('Not connected to MySQL database');
42 }
43 return `MySQL Query: ${sql}`;
44 }
45}
46
47class PostgreSQLConnection extends DatabaseConnection {
48 constructor(config) {
49 super(config);
50 this.type = 'PostgreSQL';
51 }
52
53 connect() {
54 console.log(`Connecting to PostgreSQL: ${this.config.host}:${this.config.port}`);
55 this.isConnected = true;
56 return this;
57 }
58
59 disconnect() {
60 console.log('Disconnecting from PostgreSQL');
61 this.isConnected = false;
62 return this;
63 }
64
65 query(sql) {
66 if (!this.isConnected) {
67 throw new Error('Not connected to PostgreSQL database');
68 }
69 return `PostgreSQL Query: ${sql}`;
70 }
71}
72
73// Factory class
74class DatabaseFactory {
75 static connectionTypes = {
76 mysql: MySQLConnection,
77 postgresql: PostgreSQLConnection
78 };
79
80 static createConnection(type, config) {
81 const ConnectionClass = DatabaseFactory.connectionTypes[type.toLowerCase()];
82
83 if (!ConnectionClass) {
84 throw new Error(`Unsupported database type: ${type}`);
85 }
86
87 return new ConnectionClass(config);
88 }
89
90 static registerConnectionType(type, ConnectionClass) {
91 if (!(ConnectionClass.prototype instanceof DatabaseConnection)) {
92 throw new Error('Connection class must extend DatabaseConnection');
93 }
94
95 DatabaseFactory.connectionTypes[type.toLowerCase()] = ConnectionClass;
96 }
97
98 static getSupportedTypes() {
99 return Object.keys(DatabaseFactory.connectionTypes);
100 }
101}
102
103// Usage
104const mysqlConfig = { host: 'localhost', port: 3306, database: 'myapp' };
105const pgConfig = { host: 'localhost', port: 5432, database: 'myapp' };
106
107const mysqlConn = DatabaseFactory.createConnection('mysql', mysqlConfig);
108const pgConn = DatabaseFactory.createConnection('postgresql', pgConfig);
109
110mysqlConn.connect();
111console.log(mysqlConn.query('SELECT * FROM users'));
112
113pgConn.connect();
114console.log(pgConn.query('SELECT * FROM products'));3. Observer Pattern with Classes
1// Observer pattern implementation
2class EventEmitter {
3 constructor() {
4 this._events = {};
5 }
6
7 on(event, listener) {
8 if (!this._events[event]) {
9 this._events[event] = [];
10 }
11 this._events[event].push(listener);
12 return this;
13 }
14
15 off(event, listenerToRemove) {
16 if (!this._events[event]) return this;
17
18 this._events[event] = this._events[event].filter(
19 listener => listener !== listenerToRemove
20 );
21 return this;
22 }
23
24 emit(event, data) {
25 if (!this._events[event]) return this;
26
27 this._events[event].forEach(listener => {
28 try {
29 listener(data);
30 } catch (error) {
31 console.error('Event listener error:', error);
32 }
33 });
34 return this;
35 }
36
37 once(event, listener) {
38 const onceListener = (data) => {
39 this.off(event, onceListener);
40 listener(data);
41 };
42 return this.on(event, onceListener);
43 }
44}
45
46// Observable model class
47class Model extends EventEmitter {
48 constructor(data = {}) {
49 super();
50 this._data = { ...data };
51 this._changes = {};
52 }
53
54 get(key) {
55 return this._data[key];
56 }
57
58 set(key, value) {
59 const oldValue = this._data[key];
60 this._data[key] = value;
61
62 this._changes[key] = { oldValue, newValue: value };
63
64 this.emit('change', { key, oldValue, newValue: value });
65 this.emit(`change:${key}`, { oldValue, newValue: value });
66
67 return this;
68 }
69
70 update(updates) {
71 Object.entries(updates).forEach(([key, value]) => {
72 this.set(key, value);
73 });
74 return this;
75 }
76
77 toJSON() {
78 return { ...this._data };
79 }
80
81 getChanges() {
82 return { ...this._changes };
83 }
84
85 clearChanges() {
86 this._changes = {};
87 return this;
88 }
89}
90
91// User model with validation
92class User extends Model {
93 constructor(data) {
94 super(data);
95 this.on('change', this._validate.bind(this));
96 }
97
98 _validate() {
99 const errors = [];
100
101 if (!this.get('name') || this.get('name').length < 2) {
102 errors.push('Name must be at least 2 characters');
103 }
104
105 if (!this.get('email') || !this.get('email').includes('@')) {
106 errors.push('Valid email is required');
107 }
108
109 if (errors.length > 0) {
110 this.emit('validation:error', errors);
111 } else {
112 this.emit('validation:success');
113 }
114 }
115
116 save() {
117 return new Promise((resolve, reject) => {
118 const errors = this._getValidationErrors();
119 if (errors.length > 0) {
120 reject(new Error(`Validation failed: ${errors.join(', ')}`));
121 return;
122 }
123
124 // Simulate API save
125 setTimeout(() => {
126 this.set('id', Math.random().toString(36).substr(2, 9));
127 this.emit('saved', this.toJSON());
128 this.clearChanges();
129 resolve(this);
130 }, 100);
131 });
132 }
133
134 _getValidationErrors() {
135 const errors = [];
136
137 if (!this.get('name') || this.get('name').length < 2) {
138 errors.push('Name must be at least 2 characters');
139 }
140
141 if (!this.get('email') || !this.get('email').includes('@')) {
142 errors.push('Valid email is required');
143 }
144
145 return errors;
146 }
147}
148
149// Usage
150const user = new User({ name: 'Alice', email: '[email protected]' });
151
152user.on('change', (data) => {
153 console.log(`Property changed: ${data.key} = ${data.newValue}`);
154});
155
156user.on('validation:error', (errors) => {
157 console.log('Validation errors:', errors);
158});
159
160user.on('saved', (data) => {
161 console.log('User saved:', data);
162});
163
164user.set('name', 'Alice Johnson');
165user.update({ email: '[email protected]', age: 30 });
166
167user.save().then(() => {
168 console.log('Save successful');
169}).catch(error => {
170 console.error('Save failed:', error.message);
171});Create a comprehensive task management system using ES6 classes, inheritance, and advanced patterns.
1// Task Management System with ES6 Classes
2// Demonstrates inheritance, static methods, private fields, and design patterns
3
4// Base Task class with private fields
5class Task {
6 // Private fields
7 #id;
8 #createdAt;
9 #updatedAt;
10
11 // Static counter for unique IDs
12 static #taskCounter = 0;
13
14 constructor(title, description = '') {
15 if (!title || title.trim().length === 0) {
16 throw new Error('Task title is required');
17 }
18
19 this.#id = ++Task.#taskCounter;
20 this.title = title.trim();
21 this.description = description.trim();
22 this.status = 'pending';
23 this.priority = 'medium';
24 this.tags = [];
25 this.#createdAt = new Date();
26 this.#updatedAt = new Date();
27 }
28
29 // Getters for private fields
30 get id() { return this.#id; }
31 get createdAt() { return new Date(this.#createdAt); }
32 get updatedAt() { return new Date(this.#updatedAt); }
33
34 // Private method
35 #touch() {
36 this.#updatedAt = new Date();
37 }
38
39 // Public methods
40 updateTitle(newTitle) {
41 if (!newTitle || newTitle.trim().length === 0) {
42 throw new Error('Title cannot be empty');
43 }
44 this.title = newTitle.trim();
45 this.#touch();
46 return this;
47 }
48
49 updateDescription(newDescription) {
50 this.description = newDescription.trim();
51 this.#touch();
52 return this;
53 }
54
55 setPriority(priority) {
56 const validPriorities = ['low', 'medium', 'high', 'urgent'];
57 if (!validPriorities.includes(priority)) {
58 throw new Error(`Priority must be one of: ${validPriorities.join(', ')}`);
59 }
60 this.priority = priority;
61 this.#touch();
62 return this;
63 }
64
65 addTag(tag) {
66 const cleanTag = tag.trim().toLowerCase();
67 if (cleanTag && !this.tags.includes(cleanTag)) {
68 this.tags.push(cleanTag);
69 this.#touch();
70 }
71 return this;
72 }
73
74 removeTag(tag) {
75 const index = this.tags.indexOf(tag.toLowerCase());
76 if (index > -1) {
77 this.tags.splice(index, 1);
78 this.#touch();
79 }
80 return this;
81 }
82
83 markComplete() {
84 this.status = 'completed';
85 this.completedAt = new Date();
86 this.#touch();
87 return this;
88 }
89
90 markPending() {
91 this.status = 'pending';
92 delete this.completedAt;
93 this.#touch();
94 return this;
95 }
96
97 // Virtual property
98 get isCompleted() {
99 return this.status === 'completed';
100 }
101
102 get age() {
103 return Date.now() - this.#createdAt.getTime();
104 }
105
106 // Serialization
107 toJSON() {
108 return {
109 id: this.#id,
110 title: this.title,
111 description: this.description,
112 status: this.status,
113 priority: this.priority,
114 tags: [...this.tags],
115 createdAt: this.#createdAt,
116 updatedAt: this.#updatedAt,
117 completedAt: this.completedAt
118 };
119 }
120
121 // Static methods
122 static fromJSON(data) {
123 const task = new Task(data.title, data.description);
124 task.status = data.status;
125 task.priority = data.priority;
126 task.tags = [...(data.tags || [])];
127 if (data.completedAt) {
128 task.completedAt = new Date(data.completedAt);
129 }
130 return task;
131 }
132
133 static getValidPriorities() {
134 return ['low', 'medium', 'high', 'urgent'];
135 }
136
137 static getValidStatuses() {
138 return ['pending', 'in-progress', 'completed', 'cancelled'];
139 }
140
141 toString() {
142 return `Task(${this.#id}): ${this.title} [${this.status}]`;
143 }
144}
145
146// Specialized task types using inheritance
147class RecurringTask extends Task {
148 constructor(title, description, recurrence) {
149 super(title, description);
150 this.recurrence = recurrence; // 'daily', 'weekly', 'monthly'
151 this.nextDueDate = this._calculateNextDueDate();
152 this.occurrences = [];
153 }
154
155 _calculateNextDueDate() {
156 const now = new Date();
157 const tomorrow = new Date(now);
158 tomorrow.setDate(now.getDate() + 1);
159
160 switch (this.recurrence) {
161 case 'daily':
162 return tomorrow;
163 case 'weekly':
164 const nextWeek = new Date(now);
165 nextWeek.setDate(now.getDate() + 7);
166 return nextWeek;
167 case 'monthly':
168 const nextMonth = new Date(now);
169 nextMonth.setMonth(now.getMonth() + 1);
170 return nextMonth;
171 default:
172 return tomorrow;
173 }
174 }
175
176 completeOccurrence() {
177 this.occurrences.push({
178 completedAt: new Date(),
179 dueDate: new Date(this.nextDueDate)
180 });
181
182 this.nextDueDate = this._calculateNextDueDate();
183 return this;
184 }
185
186 getCompletionHistory() {
187 return [...this.occurrences];
188 }
189
190 get isOverdue() {
191 return new Date() > this.nextDueDate;
192 }
193
194 toJSON() {
195 return {
196 ...super.toJSON(),
197 recurrence: this.recurrence,
198 nextDueDate: this.nextDueDate,
199 occurrences: [...this.occurrences]
200 };
201 }
202}
203
204class ProjectTask extends Task {
205 constructor(title, description, projectId) {
206 super(title, description);
207 this.projectId = projectId;
208 this.dependencies = [];
209 this.assignee = null;
210 this.estimatedHours = 0;
211 this.actualHours = 0;
212 }
213
214 assignTo(userId) {
215 this.assignee = userId;
216 return this;
217 }
218
219 addDependency(taskId) {
220 if (!this.dependencies.includes(taskId)) {
221 this.dependencies.push(taskId);
222 }
223 return this;
224 }
225
226 removeDependency(taskId) {
227 const index = this.dependencies.indexOf(taskId);
228 if (index > -1) {
229 this.dependencies.splice(index, 1);
230 }
231 return this;
232 }
233
234 setEstimatedHours(hours) {
235 this.estimatedHours = Math.max(0, hours);
236 return this;
237 }
238
239 logHours(hours) {
240 this.actualHours += Math.max(0, hours);
241 return this;
242 }
243
244 get isOverEstimate() {
245 return this.actualHours > this.estimatedHours;
246 }
247
248 get efficiencyRatio() {
249 return this.estimatedHours > 0 ? this.actualHours / this.estimatedHours : 0;
250 }
251
252 toJSON() {
253 return {
254 ...super.toJSON(),
255 projectId: this.projectId,
256 dependencies: [...this.dependencies],
257 assignee: this.assignee,
258 estimatedHours: this.estimatedHours,
259 actualHours: this.actualHours
260 };
261 }
262}
263
264// Task Manager class using composition and factory patterns
265class TaskManager {
266 constructor() {
267 this.tasks = new Map();
268 this.projects = new Map();
269 this.taskTypes = {
270 'basic': Task,
271 'recurring': RecurringTask,
272 'project': ProjectTask
273 };
274 }
275
276 // Factory method for creating tasks
277 createTask(type, ...args) {
278 const TaskClass = this.taskTypes[type];
279 if (!TaskClass) {
280 throw new Error(`Unknown task type: ${type}`);
281 }
282
283 const task = new TaskClass(...args);
284 this.tasks.set(task.id, task);
285 return task;
286 }
287
288 // CRUD operations
289 getTask(id) {
290 return this.tasks.get(id);
291 }
292
293 getAllTasks() {
294 return Array.from(this.tasks.values());
295 }
296
297 updateTask(id, updates) {
298 const task = this.tasks.get(id);
299 if (!task) {
300 throw new Error(`Task with ID ${id} not found`);
301 }
302
303 Object.entries(updates).forEach(([key, value]) => {
304 if (typeof task[key] !== 'undefined' && key !== 'id') {
305 task[key] = value;
306 }
307 });
308
309 return task;
310 }
311
312 deleteTask(id) {
313 const deleted = this.tasks.delete(id);
314 if (deleted) {
315 // Remove dependencies in other tasks
316 this.getAllTasks().forEach(task => {
317 if (task instanceof ProjectTask) {
318 task.removeDependency(id);
319 }
320 });
321 }
322 return deleted;
323 }
324
325 // Query methods
326 getTasksByStatus(status) {
327 return this.getAllTasks().filter(task => task.status === status);
328 }
329
330 getTasksByPriority(priority) {
331 return this.getAllTasks().filter(task => task.priority === priority);
332 }
333
334 getTasksByTag(tag) {
335 return this.getAllTasks().filter(task => task.tags.includes(tag.toLowerCase()));
336 }
337
338 getOverdueTasks() {
339 return this.getAllTasks().filter(task => {
340 if (task instanceof RecurringTask) {
341 return task.isOverdue;
342 }
343 return false;
344 });
345 }
346
347 getTasksByProject(projectId) {
348 return this.getAllTasks().filter(task =>
349 task instanceof ProjectTask && task.projectId === projectId
350 );
351 }
352
353 // Analytics
354 getTaskStats() {
355 const tasks = this.getAllTasks();
356 const stats = {
357 total: tasks.length,
358 byStatus: {},
359 byPriority: {},
360 byType: {},
361 averageAge: 0
362 };
363
364 let totalAge = 0;
365
366 tasks.forEach(task => {
367 // By status
368 stats.byStatus[task.status] = (stats.byStatus[task.status] || 0) + 1;
369
370 // By priority
371 stats.byPriority[task.priority] = (stats.byPriority[task.priority] || 0) + 1;
372
373 // By type
374 const type = task.constructor.name;
375 stats.byType[type] = (stats.byType[type] || 0) + 1;
376
377 // Age calculation
378 totalAge += task.age;
379 });
380
381 stats.averageAge = tasks.length > 0 ? totalAge / tasks.length : 0;
382
383 return stats;
384 }
385
386 // Bulk operations
387 completeTasks(taskIds) {
388 const results = [];
389 taskIds.forEach(id => {
390 const task = this.getTask(id);
391 if (task) {
392 task.markComplete();
393 results.push({ id, success: true });
394 } else {
395 results.push({ id, success: false, error: 'Task not found' });
396 }
397 });
398 return results;
399 }
400
401 // Export/Import
402 exportTasks() {
403 return {
404 tasks: this.getAllTasks().map(task => task.toJSON()),
405 exportedAt: new Date().toISOString(),
406 version: '1.0'
407 };
408 }
409
410 importTasks(data) {
411 if (!data.tasks || !Array.isArray(data.tasks)) {
412 throw new Error('Invalid import data');
413 }
414
415 const imported = [];
416 data.tasks.forEach(taskData => {
417 try {
418 const task = Task.fromJSON(taskData);
419 this.tasks.set(task.id, task);
420 imported.push(task.id);
421 } catch (error) {
422 console.error('Failed to import task:', taskData, error);
423 }
424 });
425
426 return imported;
427 }
428}
429
430// Usage demonstration
431console.log('=== Task Management System Demo ===\n');
432
433// Create task manager instance
434const taskManager = new TaskManager();
435
436// Create different types of tasks
437const basicTask = taskManager.createTask('basic', 'Review code', 'Review pull request #123');
438basicTask.setPriority('high').addTag('code-review').addTag('urgent');
439
440const recurringTask = taskManager.createTask('recurring', 'Daily standup', 'Attend team standup meeting', 'daily');
441
442const projectTask = taskManager.createTask('project', 'Implement user auth', 'Add user authentication system', 'PROJECT-001');
443projectTask.assignTo('dev-123').setEstimatedHours(8).logHours(3);
444
445console.log('1. Created Tasks:');
446console.log(`Basic Task: ${basicTask.toString()}`);
447console.log(`Recurring Task: ${recurringTask.toString()}`);
448console.log(`Project Task: ${projectTask.toString()}`);
449
450// Complete some tasks
451basicTask.markComplete();
452recurringTask.completeOccurrence();
453
454console.log('\n2. Task Operations:');
455console.log(`Basic task completed: ${basicTask.isCompleted}`);
456console.log(`Recurring task next due: ${recurringTask.nextDueDate.toLocaleDateString()}`);
457console.log(`Project task efficiency: ${projectTask.efficiencyRatio.toFixed(2)}`);
458
459// Query tasks
460console.log('\n3. Task Queries:');
461console.log(`High priority tasks: ${taskManager.getTasksByPriority('high').length}`);
462console.log(`Completed tasks: ${taskManager.getTasksByStatus('completed').length}`);
463console.log(`Tasks with 'urgent' tag: ${taskManager.getTasksByTag('urgent').length}`);
464
465// Get statistics
466const stats = taskManager.getTaskStats();
467console.log('\n4. Task Statistics:');
468console.log(`Total tasks: ${stats.total}`);
469console.log(`By status: ${JSON.stringify(stats.byStatus)}`);
470console.log(`By priority: ${JSON.stringify(stats.byPriority)}`);
471console.log(`By type: ${JSON.stringify(stats.byType)}`);
472console.log(`Average age: ${Math.round(stats.averageAge / 1000)} seconds`);
473
474// Demonstrate inheritance and polymorphism
475console.log('\n5. Inheritance and Polymorphism:');
476const tasks = taskManager.getAllTasks();
477tasks.forEach(task => {
478 console.log(`${task.constructor.name}: ${task.title} - ${task.status}`);
479
480 // Different behavior based on type
481 if (task instanceof RecurringTask) {
482 console.log(` Next due: ${task.nextDueDate.toDateString()}`);
483 } else if (task instanceof ProjectTask) {
484 console.log(` Project: ${task.projectId}, Assignee: ${task.assignee}`);
485 }
486});
487
488// Export data
489const exportData = taskManager.exportTasks();
490console.log(`\n6. Export: ${exportData.tasks.length} tasks exported`);
491
492// Demonstrate static methods
493console.log('\n7. Static Methods:');
494console.log(`Valid priorities: ${Task.getValidPriorities().join(', ')}`);
495console.log(`Valid statuses: ${Task.getValidStatuses().join(', ')}`);
496
497// Create task from JSON
498const taskFromJSON = Task.fromJSON(exportData.tasks[0]);
499console.log(`Recreated task: ${taskFromJSON.toString()}`);
500
501console.log('\n=== Demo Complete ===');
502console.log('\nFeatures Demonstrated:');
503console.log('✓ ES6 Class syntax with constructor methods');
504console.log('✓ Private fields and methods (#syntax)');
505console.log('✓ Static methods and properties');
506console.log('✓ Inheritance with extends and super');
507console.log('✓ Method overriding and polymorphism');
508console.log('✓ Getters and setters');
509console.log('✓ Factory pattern with class methods');
510console.log('✓ Composition and aggregation');
511console.log('✓ Abstract class patterns');
512console.log('✓ Data serialization and deserialization');Your progress is saved in this browser, on this device only.