Explore new ES6 data types: Symbols for unique identifiers, Sets for unique values, and Maps for key-value pairs.
ES6 introduced powerful new data structures that extend JavaScript's capabilities beyond traditional arrays and objects. Symbols provide unique identifiers that avoid naming collisions, Sets offer collections of unique values with built-in methods, and Maps provide key-value storage with any type of key and enhanced iteration capabilities.
These data structures solve common programming challenges: Symbols enable private-like properties and prevent naming conflicts, Sets efficiently manage unique collections without duplicates, and Maps provide more flexible key-value storage than plain objects. Understanding these structures is crucial for modern JavaScript development, as they're used extensively in frameworks, libraries, and advanced JavaScript patterns.
These features are particularly valuable in large applications where you need guaranteed uniqueness (Symbols), efficient duplicate removal (Sets), or flexible key-value mapping (Maps). They also provide better performance characteristics and more intuitive APIs for specific use cases than traditional JavaScript data structures.
Symbols are a unique primitive data type introduced in ES6. Every Symbol is unique, making them perfect for creating private-like properties, avoiding naming collisions, and implementing advanced JavaScript patterns.
Creating and Using Symbols:
1// Creating symbols
2const symbol1 = Symbol();
3const symbol2 = Symbol('description');
4const symbol3 = Symbol('description');
5
6console.log(symbol1); // Symbol()
7console.log(symbol2); // Symbol(description)
8console.log(symbol3); // Symbol(description)
9
10// Every symbol is unique, even with the same description
11console.log(symbol2 === symbol3); // false
12console.log(symbol1 === symbol2); // false
13
14// Type checking
15console.log(typeof symbol1); // 'symbol'
16
17// Symbols cannot be coerced to strings automatically
18// console.log("Hello " + symbol1); // TypeError
19console.log(`Hello ${symbol1.toString()}`); // "Hello Symbol()"
20console.log(`Hello ${String(symbol1)}`); // "Hello Symbol()"Symbols as Object Properties:
1// Using symbols as object property keys
2const nameSymbol = Symbol('name');
3const ageSymbol = Symbol('age');
4const secretSymbol = Symbol('secret');
5
6const person = {
7 [nameSymbol]: 'John Doe',
8 [ageSymbol]: 30,
9 [secretSymbol]: 'hidden value',
10 // Regular string property
11 email: '[email protected]'
12};
13
14// Accessing symbol properties
15console.log(person[nameSymbol]); // 'John Doe'
16console.log(person[ageSymbol]); // 30
17console.log(person[secretSymbol]); // 'hidden value'
18
19// Symbol properties are not enumerable in normal iteration
20console.log(Object.keys(person)); // ['email']
21console.log(Object.values(person)); // ['[email protected]']
22
23// But they can be accessed if you have the symbol reference
24const symbols = Object.getOwnPropertySymbols(person);
25console.log(symbols); // [Symbol(name), Symbol(age), Symbol(secret)]
26
27// Getting both string and symbol keys
28const allKeys = [...Object.keys(person), ...Object.getOwnPropertySymbols(person)];
29console.log(allKeys); // ['email', Symbol(name), Symbol(age), Symbol(secret)]Global Symbol Registry:
1// Global symbols with Symbol.for() and Symbol.keyFor()
2const globalSymbol1 = Symbol.for('shared');
3const globalSymbol2 = Symbol.for('shared');
4
5// Global symbols with the same key are identical
6console.log(globalSymbol1 === globalSymbol2); // true
7
8// Get the key for a global symbol
9console.log(Symbol.keyFor(globalSymbol1)); // 'shared'
10
11// Regular symbols are not in the global registry
12const localSymbol = Symbol('local');
13console.log(Symbol.keyFor(localSymbol)); // undefined
14
15// Useful for creating shared symbols across different parts of an application
16const LOG_LEVEL = Symbol.for('app.logLevel');
17const DEBUG_MODE = Symbol.for('app.debugMode');
18
19// These can be accessed from anywhere in the application
20function getLogLevel() {
21 return Symbol.for('app.logLevel');
22}Well-Known Symbols:
1// ES6 defines several well-known symbols for meta-programming
2class CustomIterable {
3 constructor(...items) {
4 this.items = items;
5 }
6
7 // Symbol.iterator makes objects iterable
8 *[Symbol.iterator]() {
9 for (const item of this.items) {
10 yield item;
11 }
12 }
13
14 // Symbol.toStringTag customizes Object.prototype.toString
15 get [Symbol.toStringTag]() {
16 return 'CustomIterable';
17 }
18}
19
20const iterable = new CustomIterable(1, 2, 3, 4);
21
22// Can be used with for...of because of Symbol.iterator
23for (const value of iterable) {
24 console.log(value); // 1, 2, 3, 4
25}
26
27// Custom toString tag
28console.log(Object.prototype.toString.call(iterable)); // [object CustomIterable]
29
30// Other well-known symbols
31class MathUtils {
32 static [Symbol.hasInstance](instance) {
33 return typeof instance === 'number';
34 }
35}
36
37console.log(42 instanceof MathUtils); // true (using custom Symbol.hasInstance)
38console.log('hello' instanceof MathUtils); // falsePrivate-like Properties with Symbols:
1// Using symbols for pseudo-private properties
2const _balance = Symbol('balance');
3const _transactions = Symbol('transactions');
4const _validateAmount = Symbol('validateAmount');
5
6class BankAccount {
7 constructor(initialBalance = 0) {
8 this[_balance] = initialBalance;
9 this[_transactions] = [];
10 }
11
12 // Private-like method using symbol
13 [_validateAmount](amount) {
14 return typeof amount === 'number' && amount > 0;
15 }
16
17 deposit(amount) {
18 if (!this[_validateAmount](amount)) {
19 throw new Error('Invalid amount');
20 }
21
22 this[_balance] += amount;
23 this[_transactions].push({
24 type: 'deposit',
25 amount,
26 date: new Date(),
27 balance: this[_balance]
28 });
29
30 return this[_balance];
31 }
32
33 withdraw(amount) {
34 if (!this[_validateAmount](amount)) {
35 throw new Error('Invalid amount');
36 }
37
38 if (amount > this[_balance]) {
39 throw new Error('Insufficient funds');
40 }
41
42 this[_balance] -= amount;
43 this[_transactions].push({
44 type: 'withdrawal',
45 amount: -amount,
46 date: new Date(),
47 balance: this[_balance]
48 });
49
50 return this[_balance];
51 }
52
53 get balance() {
54 return this[_balance];
55 }
56
57 get transactionHistory() {
58 return [...this[_transactions]]; // Return a copy
59 }
60}
61
62const account = new BankAccount(1000);
63console.log(account.balance); // 1000
64
65account.deposit(500);
66account.withdraw(200);
67
68console.log(account.balance); // 1300
69console.log(account.transactionHistory.length); // 2
70
71// These properties are not easily accessible from outside
72console.log(Object.keys(account)); // []
73console.log(account._balance); // undefined (not accessible without the symbol)
74
75// But they can still be discovered with Object.getOwnPropertySymbols
76const accountSymbols = Object.getOwnPropertySymbols(account);
77console.log(accountSymbols.length); // 2 (balance and transactions symbols)Symbols provide a powerful mechanism for creating unique identifiers and implementing advanced patterns while maintaining better encapsulation than traditional approaches.
Sets are collections that store unique values of any type. They provide efficient methods for managing collections where duplicates are not allowed, making them perfect for scenarios like tag management, user permissions, or deduplication.
Creating and Basic Operations:
1// Creating Sets
2const emptySet = new Set();
3const numbersSet = new Set([1, 2, 3, 4, 5]);
4const stringsSet = new Set(['apple', 'banana', 'orange']);
5const mixedSet = new Set([1, 'hello', true, null, undefined]);
6
7// Sets automatically remove duplicates
8const duplicateSet = new Set([1, 2, 2, 3, 3, 3, 4]);
9console.log(duplicateSet); // Set { 1, 2, 3, 4 }
10console.log(duplicateSet.size); // 4
11
12// Adding values
13const tags = new Set();
14tags.add('javascript');
15tags.add('web-development');
16tags.add('frontend');
17tags.add('javascript'); // Duplicate - won't be added
18
19console.log(tags); // Set { 'javascript', 'web-development', 'frontend' }
20console.log(tags.size); // 3
21
22// Checking if value exists
23console.log(tags.has('javascript')); // true
24console.log(tags.has('python')); // false
25
26// Removing values
27tags.delete('frontend');
28console.log(tags.has('frontend')); // false
29console.log(tags.size); // 2
30
31// Clearing all values
32const tempSet = new Set([1, 2, 3]);
33tempSet.clear();
34console.log(tempSet.size); // 0Iteration Methods:
1// Sets are iterable
2const colors = new Set(['red', 'green', 'blue', 'yellow']);
3
4// for...of loop
5console.log('Using for...of:');
6for (const color of colors) {
7 console.log(color);
8}
9
10// forEach method
11console.log('\nUsing forEach:');
12colors.forEach((color, colorAgain, set) => {
13 console.log(color); // Note: value is passed twice (no key concept in Sets)
14});
15
16// Converting to array
17const colorsArray = Array.from(colors);
18// or
19const colorsSpread = [...colors];
20console.log(colorsArray); // ['red', 'green', 'blue', 'yellow']
21
22// Using array methods on Sets
23const upperCaseColors = [...colors].map(color => color.toUpperCase());
24console.log(upperCaseColors); // ['RED', 'GREEN', 'BLUE', 'YELLOW']
25
26// Set methods for iteration
27console.log('\nSet iterators:');
28for (const value of colors.values()) {
29 console.log('Value:', value);
30}
31
32for (const value of colors.keys()) { // Same as values() in Sets
33 console.log('Key:', value);
34}
35
36for (const [value, valueAgain] of colors.entries()) {
37 console.log('Entry:', value, valueAgain); // Both are the same
38}Set Operations and Utilities:
1// Set operations (union, intersection, difference)
2const setA = new Set([1, 2, 3, 4, 5]);
3const setB = new Set([4, 5, 6, 7, 8]);
4
5// Union (combine all unique values)
6const union = new Set([...setA, ...setB]);
7console.log(union); // Set { 1, 2, 3, 4, 5, 6, 7, 8 }
8
9// Intersection (common values)
10const intersection = new Set([...setA].filter(x => setB.has(x)));
11console.log(intersection); // Set { 4, 5 }
12
13// Difference (values in A but not in B)
14const difference = new Set([...setA].filter(x => !setB.has(x)));
15console.log(difference); // Set { 1, 2, 3 }
16
17// Symmetric difference (values in either A or B, but not both)
18const symmetricDiff = new Set([
19 ...[...setA].filter(x => !setB.has(x)),
20 ...[...setB].filter(x => !setA.has(x))
21]);
22console.log(symmetricDiff); // Set { 1, 2, 3, 6, 7, 8 }
23
24// Utility functions for Set operations
25class SetUtils {
26 static union(setA, setB) {
27 return new Set([...setA, ...setB]);
28 }
29
30 static intersection(setA, setB) {
31 return new Set([...setA].filter(x => setB.has(x)));
32 }
33
34 static difference(setA, setB) {
35 return new Set([...setA].filter(x => !setB.has(x)));
36 }
37
38 static isSubset(subset, superset) {
39 return [...subset].every(x => superset.has(x));
40 }
41
42 static isSuperset(superset, subset) {
43 return this.isSubset(subset, superset);
44 }
45
46 static areDisjoint(setA, setB) {
47 return this.intersection(setA, setB).size === 0;
48 }
49}
50
51// Test utility functions
52const set1 = new Set([1, 2, 3]);
53const set2 = new Set([2, 3, 4]);
54const set3 = new Set([1, 2]);
55
56console.log('Union:', SetUtils.union(set1, set2)); // Set { 1, 2, 3, 4 }
57console.log('Intersection:', SetUtils.intersection(set1, set2)); // Set { 2, 3 }
58console.log('Is set3 subset of set1?', SetUtils.isSubset(set3, set1)); // true
59console.log('Are set1 and set2 disjoint?', SetUtils.areDisjoint(set1, set2)); // falsePractical Set Use Cases:
1// Use case 1: Removing duplicates from arrays
2function removeDuplicates(array) {
3 return [...new Set(array)];
4}
5
6const numbersWithDuplicates = [1, 2, 2, 3, 4, 4, 5, 1];
7const uniqueNumbers = removeDuplicates(numbersWithDuplicates);
8console.log(uniqueNumbers); // [1, 2, 3, 4, 5]
9
10// Use case 2: Tag system
11class Article {
12 constructor(title, content) {
13 this.title = title;
14 this.content = content;
15 this.tags = new Set();
16 this.categories = new Set();
17 }
18
19 addTag(tag) {
20 this.tags.add(tag.toLowerCase());
21 return this;
22 }
23
24 addTags(...tags) {
25 tags.forEach(tag => this.addTag(tag));
26 return this;
27 }
28
29 removeTag(tag) {
30 this.tags.delete(tag.toLowerCase());
31 return this;
32 }
33
34 hasTag(tag) {
35 return this.tags.has(tag.toLowerCase());
36 }
37
38 addCategory(category) {
39 this.categories.add(category);
40 return this;
41 }
42
43 getTagsArray() {
44 return [...this.tags];
45 }
46
47 getCategoriesArray() {
48 return [...this.categories];
49 }
50
51 // Find common tags with another article
52 commonTagsWith(otherArticle) {
53 return SetUtils.intersection(this.tags, otherArticle.tags);
54 }
55
56 // Get all unique tags from this article and another
57 allTagsWith(otherArticle) {
58 return SetUtils.union(this.tags, otherArticle.tags);
59 }
60}
61
62const article1 = new Article('JavaScript Basics', 'Learning JS...')
63 .addTags('JavaScript', 'Programming', 'Web Development', 'Frontend')
64 .addCategory('Tutorial');
65
66const article2 = new Article('React Guide', 'React framework...')
67 .addTags('React', 'JavaScript', 'Frontend', 'Components')
68 .addCategory('Tutorial');
69
70console.log('Article 1 tags:', article1.getTagsArray());
71console.log('Article 2 tags:', article2.getTagsArray());
72console.log('Common tags:', [...article1.commonTagsWith(article2)]);
73console.log('All unique tags:', [...article1.allTagsWith(article2)]);
74
75// Use case 3: User permissions system
76class User {
77 constructor(name) {
78 this.name = name;
79 this.permissions = new Set();
80 this.roles = new Set();
81 }
82
83 grantPermission(permission) {
84 this.permissions.add(permission);
85 return this;
86 }
87
88 revokePermission(permission) {
89 this.permissions.delete(permission);
90 return this;
91 }
92
93 hasPermission(permission) {
94 return this.permissions.has(permission);
95 }
96
97 addRole(role) {
98 this.roles.add(role);
99 // Grant role-specific permissions
100 const rolePermissions = this.getRolePermissions(role);
101 rolePermissions.forEach(permission => this.grantPermission(permission));
102 return this;
103 }
104
105 removeRole(role) {
106 this.roles.delete(role);
107 // Would need more complex logic to remove role permissions
108 // without affecting permissions from other roles
109 return this;
110 }
111
112 getRolePermissions(role) {
113 const rolePermissionMap = {
114 'admin': ['read', 'write', 'delete', 'manage_users'],
115 'editor': ['read', 'write'],
116 'viewer': ['read']
117 };
118 return rolePermissionMap[role] || [];
119 }
120
121 getAllPermissions() {
122 return [...this.permissions];
123 }
124}
125
126const user = new User('John Doe')
127 .addRole('editor')
128 .grantPermission('export_data');
129
130console.log('User permissions:', user.getAllPermissions());
131console.log('Can write?', user.hasPermission('write')); // true
132console.log('Can delete?', user.hasPermission('delete')); // falseSets provide efficient and intuitive ways to work with unique collections, making them invaluable for deduplication, membership testing, and set theory operations.
Maps are collections of key-value pairs where keys can be of any type, including objects, functions, and primitives. Unlike plain objects, Maps maintain insertion order, have better performance characteristics for frequent additions and deletions, and provide cleaner iteration methods.
Creating and Basic Operations:
1// Creating Maps
2const emptyMap = new Map();
3const numbersMap = new Map([
4 [1, 'one'],
5 [2, 'two'],
6 [3, 'three']
7]);
8
9// Maps can have any type of key
10const mixedMap = new Map([
11 ['string-key', 'string value'],
12 [42, 'number key'],
13 [true, 'boolean key'],
14 [{}, 'object key'],
15 [function() {}, 'function key']
16]);
17
18console.log(mixedMap.size); // 5
19
20// Setting and getting values
21const userMap = new Map();
22
23// Setting values
24userMap.set('name', 'John Doe');
25userMap.set('age', 30);
26userMap.set('email', '[email protected]');
27
28// Method chaining is supported
29userMap.set('city', 'New York').set('country', 'USA');
30
31// Getting values
32console.log(userMap.get('name')); // 'John Doe'
33console.log(userMap.get('age')); // 30
34console.log(userMap.get('nonexistent')); // undefined
35
36// Checking if key exists
37console.log(userMap.has('email')); // true
38console.log(userMap.has('phone')); // false
39
40// Getting size
41console.log(userMap.size); // 5
42
43// Deleting entries
44userMap.delete('city');
45console.log(userMap.has('city')); // false
46console.log(userMap.size); // 4
47
48// Clearing all entries
49const tempMap = new Map([['a', 1], ['b', 2]]);
50tempMap.clear();
51console.log(tempMap.size); // 0Advanced Key Types:
1// Using objects as keys
2const user1 = { id: 1, name: 'Alice' };
3const user2 = { id: 2, name: 'Bob' };
4const user3 = { id: 3, name: 'Charlie' };
5
6const userScores = new Map([
7 [user1, 95],
8 [user2, 87],
9 [user3, 92]
10]);
11
12console.log(userScores.get(user1)); // 95
13console.log(userScores.get(user2)); // 87
14
15// Functions as keys
16const cache = new Map();
17const expensiveFunction = (x) => x * x * x;
18const anotherFunction = (x) => x + x;
19
20cache.set(expensiveFunction, new Map());
21cache.set(anotherFunction, new Map());
22
23// Store function results
24cache.get(expensiveFunction).set(5, 125); // Cache result of expensiveFunction(5)
25cache.get(anotherFunction).set(5, 10); // Cache result of anotherFunction(5)
26
27console.log(cache.get(expensiveFunction).get(5)); // 125
28
29// Symbols as keys
30const privateData = Symbol('private');
31const publicData = Symbol('public');
32
33const dataMap = new Map([
34 [privateData, { secret: 'hidden info' }],
35 [publicData, { info: 'public info' }]
36]);
37
38console.log(dataMap.get(privateData)); // { secret: 'hidden info' }
39console.log(dataMap.get(publicData)); // { info: 'public info' }
40
41// Dates as keys
42const eventMap = new Map();
43const today = new Date('2024-01-15');
44const tomorrow = new Date('2024-01-16');
45
46eventMap.set(today, ['Meeting at 10am', 'Lunch at 12pm']);
47eventMap.set(tomorrow, ['Conference call at 2pm']);
48
49console.log(eventMap.get(today)); // ['Meeting at 10am', 'Lunch at 12pm']Iteration Methods:
1// Maps are iterable and maintain insertion order
2const fruitPrices = new Map([
3 ['apple', 1.50],
4 ['banana', 0.75],
5 ['orange', 2.00],
6 ['grape', 3.50]
7]);
8
9// for...of with destructuring
10console.log('Using for...of with entries:');
11for (const [fruit, price] of fruitPrices) {
12 console.log(`${fruit}: $${price}`);
13}
14
15// forEach method
16console.log('\nUsing forEach:');
17fruitPrices.forEach((price, fruit, map) => {
18 console.log(`${fruit}: $${price}`);
19});
20
21// Iterating over keys only
22console.log('\nKeys:');
23for (const fruit of fruitPrices.keys()) {
24 console.log(fruit);
25}
26
27// Iterating over values only
28console.log('\nValues:');
29for (const price of fruitPrices.values()) {
30 console.log(`$${price}`);
31}
32
33// Converting to arrays
34const entriesArray = Array.from(fruitPrices);
35const entriesSpread = [...fruitPrices];
36console.log('Entries as array:', entriesArray);
37
38const keysArray = [...fruitPrices.keys()];
39const valuesArray = [...fruitPrices.values()];
40console.log('Keys:', keysArray);
41console.log('Values:', valuesArray);
42
43// Converting from arrays
44const newMap = new Map([
45 ['key1', 'value1'],
46 ['key2', 'value2']
47]);Maps vs Objects Comparison:
1// Performance comparison and feature differences
2console.log('=== Maps vs Objects ===');
3
4// Object approach
5const obj = {
6 name: 'John',
7 age: 30,
8 'email': '[email protected]'
9};
10
11// Map approach
12const map = new Map([
13 ['name', 'John'],
14 ['age', 30],
15 ['email', '[email protected]']
16]);
17
18// Size comparison
19console.log('Object properties count:', Object.keys(obj).length); // Manual counting
20console.log('Map size:', map.size); // Built-in size property
21
22// Key types
23// Objects: only strings and symbols as keys
24const objWithNumericKey = { 1: 'one', '1': 'string one' }; // Both become strings
25console.log(objWithNumericKey[1]); // 'string one' (overwrote numeric key)
26
27// Maps: any type as key
28const mapWithNumericKey = new Map([[1, 'one'], ['1', 'string one']]);
29console.log(mapWithNumericKey.get(1)); // 'one'
30console.log(mapWithNumericKey.get('1')); // 'string one'
31
32// Iteration order
33// Objects: not guaranteed (though mostly insertion order in modern engines)
34// Maps: guaranteed insertion order
35
36// Inheritance
37// Objects: have prototype with default keys
38console.log('toString' in obj); // true (inherited from prototype)
39
40// Maps: no default keys
41console.log(map.has('toString')); // false (clean, no prototype)
42
43// Performance characteristics
44function performanceTest() {
45 const iterations = 100000;
46
47 // Object performance
48 console.time('Object creation and access');
49 const testObj = {};
50 for (let i = 0; i < iterations; i++) {
51 testObj[`key${i}`] = i;
52 }
53 for (let i = 0; i < iterations; i++) {
54 const value = testObj[`key${i}`];
55 }
56 console.timeEnd('Object creation and access');
57
58 // Map performance
59 console.time('Map creation and access');
60 const testMap = new Map();
61 for (let i = 0; i < iterations; i++) {
62 testMap.set(`key${i}`, i);
63 }
64 for (let i = 0; i < iterations; i++) {
65 const value = testMap.get(`key${i}`);
66 }
67 console.timeEnd('Map creation and access');
68}
69
70performanceTest();Practical Map Use Cases:
1// Use case 1: Caching system
2class SimpleCache {
3 constructor(maxSize = 100) {
4 this.cache = new Map();
5 this.maxSize = maxSize;
6 }
7
8 get(key) {
9 if (this.cache.has(key)) {
10 // Move to end (most recently used)
11 const value = this.cache.get(key);
12 this.cache.delete(key);
13 this.cache.set(key, value);
14 return value;
15 }
16 return null;
17 }
18
19 set(key, value) {
20 if (this.cache.has(key)) {
21 // Update existing
22 this.cache.delete(key);
23 } else if (this.cache.size >= this.maxSize) {
24 // Remove least recently used (first entry)
25 const firstKey = this.cache.keys().next().value;
26 this.cache.delete(firstKey);
27 }
28
29 this.cache.set(key, value);
30 }
31
32 has(key) {
33 return this.cache.has(key);
34 }
35
36 clear() {
37 this.cache.clear();
38 }
39
40 size() {
41 return this.cache.size;
42 }
43
44 keys() {
45 return [...this.cache.keys()];
46 }
47}
48
49const cache = new SimpleCache(3);
50cache.set('a', 1);
51cache.set('b', 2);
52cache.set('c', 3);
53console.log('Cache keys after filling:', cache.keys()); // ['a', 'b', 'c']
54
55cache.get('a'); // Move 'a' to end
56cache.set('d', 4); // Should remove 'b' (least recently used)
57console.log('Cache keys after adding d:', cache.keys()); // ['c', 'a', 'd']
58
59// Use case 2: Event system with object keys
60class EventEmitter {
61 constructor() {
62 this.events = new Map();
63 }
64
65 on(event, callback) {
66 if (!this.events.has(event)) {
67 this.events.set(event, new Set());
68 }
69 this.events.get(event).add(callback);
70 }
71
72 off(event, callback) {
73 if (this.events.has(event)) {
74 this.events.get(event).delete(callback);
75 if (this.events.get(event).size === 0) {
76 this.events.delete(event);
77 }
78 }
79 }
80
81 emit(event, ...args) {
82 if (this.events.has(event)) {
83 this.events.get(event).forEach(callback => {
84 try {
85 callback(...args);
86 } catch (error) {
87 console.error('Event callback error:', error);
88 }
89 });
90 }
91 }
92
93 listEvents() {
94 return [...this.events.keys()];
95 }
96
97 getListenerCount(event) {
98 return this.events.has(event) ? this.events.get(event).size : 0;
99 }
100}
101
102const emitter = new EventEmitter();
103
104const loginHandler = (user) => console.log(`User logged in: ${user}`);
105const analyticsHandler = (user) => console.log(`Analytics: Login by ${user}`);
106
107emitter.on('login', loginHandler);
108emitter.on('login', analyticsHandler);
109emitter.on('logout', (user) => console.log(`User logged out: ${user}`));
110
111emitter.emit('login', 'John Doe');
112// Output:
113// User logged in: John Doe
114// Analytics: Login by John Doe
115
116console.log('Events:', emitter.listEvents()); // ['login', 'logout']
117console.log('Login listeners:', emitter.getListenerCount('login')); // 2
118
119// Use case 3: Metadata system
120class MetadataManager {
121 constructor() {
122 this.metadata = new Map();
123 }
124
125 setMetadata(object, key, value) {
126 if (!this.metadata.has(object)) {
127 this.metadata.set(object, new Map());
128 }
129 this.metadata.get(object).set(key, value);
130 }
131
132 getMetadata(object, key) {
133 const objectMeta = this.metadata.get(object);
134 return objectMeta ? objectMeta.get(key) : undefined;
135 }
136
137 getAllMetadata(object) {
138 const objectMeta = this.metadata.get(object);
139 return objectMeta ? Object.fromEntries(objectMeta) : {};
140 }
141
142 hasMetadata(object, key) {
143 const objectMeta = this.metadata.get(object);
144 return objectMeta ? objectMeta.has(key) : false;
145 }
146
147 deleteMetadata(object, key) {
148 const objectMeta = this.metadata.get(object);
149 if (objectMeta) {
150 objectMeta.delete(key);
151 if (objectMeta.size === 0) {
152 this.metadata.delete(object);
153 }
154 }
155 }
156
157 clearObject(object) {
158 this.metadata.delete(object);
159 }
160}
161
162const metaManager = new MetadataManager();
163const button1 = document.createElement('button');
164const button2 = document.createElement('button');
165
166// Attach metadata to DOM elements
167metaManager.setMetadata(button1, 'component', 'SubmitButton');
168metaManager.setMetadata(button1, 'theme', 'primary');
169metaManager.setMetadata(button1, 'size', 'large');
170
171metaManager.setMetadata(button2, 'component', 'CancelButton');
172metaManager.setMetadata(button2, 'theme', 'secondary');
173
174console.log('Button1 metadata:', metaManager.getAllMetadata(button1));
175console.log('Button2 component:', metaManager.getMetadata(button2, 'component'));Maps provide powerful, flexible key-value storage with better performance characteristics and more intuitive APIs than plain objects for many use cases.
Advanced usage patterns combine Symbols, Sets, and Maps for powerful solutions. WeakMap and WeakSet provide garbage collection-friendly alternatives for scenarios where you don't want to prevent objects from being cleaned up.
Combined Usage Patterns:
1// Pattern 1: Private data with Symbols and Maps
2const privateData = new Map();
3const _id = Symbol('id');
4const _name = Symbol('name');
5const _permissions = Symbol('permissions');
6
7class SecureUser {
8 constructor(id, name) {
9 // Use symbols as keys for the main object
10 this[_id] = id;
11 this[_name] = name;
12 this[_permissions] = new Set();
13
14 // Use Map with this instance as key for additional private data
15 privateData.set(this, {
16 createdAt: new Date(),
17 accessLog: [],
18 sessionTokens: new Set()
19 });
20 }
21
22 get id() {
23 return this[_id];
24 }
25
26 get name() {
27 return this[_name];
28 }
29
30 addPermission(permission) {
31 this[_permissions].add(permission);
32 this.logAccess(`Permission added: ${permission}`);
33 }
34
35 hasPermission(permission) {
36 return this[_permissions].has(permission);
37 }
38
39 getPermissions() {
40 return [...this[_permissions]];
41 }
42
43 addSessionToken(token) {
44 const data = privateData.get(this);
45 data.sessionTokens.add(token);
46 this.logAccess('Session token added');
47 }
48
49 removeSessionToken(token) {
50 const data = privateData.get(this);
51 data.sessionTokens.delete(token);
52 this.logAccess('Session token removed');
53 }
54
55 hasValidSession(token) {
56 const data = privateData.get(this);
57 return data.sessionTokens.has(token);
58 }
59
60 logAccess(action) {
61 const data = privateData.get(this);
62 data.accessLog.push({
63 action,
64 timestamp: new Date()
65 });
66 }
67
68 getAccessLog() {
69 const data = privateData.get(this);
70 return [...data.accessLog];
71 }
72
73 getCreatedAt() {
74 const data = privateData.get(this);
75 return data.createdAt;
76 }
77
78 // Clean up method
79 destroy() {
80 privateData.delete(this);
81 }
82}
83
84const user = new SecureUser(1, 'John Doe');
85user.addPermission('read');
86user.addPermission('write');
87user.addSessionToken('abc123');
88
89console.log('User ID:', user.id);
90console.log('Permissions:', user.getPermissions());
91console.log('Has read permission:', user.hasPermission('read'));
92console.log('Valid session:', user.hasValidSession('abc123'));
93console.log('Access log:', user.getAccessLog());
94
95// Private data is not easily accessible
96console.log('Direct access to private data:', user._privateData); // undefined
97console.log('Symbol properties:', Object.getOwnPropertySymbols(user)); // Shows symbols but values are not easily accessibleWeakMap and WeakSet:
1// WeakMap: Keys must be objects and don't prevent garbage collection
2const objectMetadata = new WeakMap();
3const objectRelations = new WeakMap();
4
5class ElementManager {
6 constructor() {
7 this.elements = new WeakMap();
8 this.relationships = new WeakMap();
9 }
10
11 // Attach metadata to DOM elements without preventing GC
12 attachMetadata(element, metadata) {
13 this.elements.set(element, {
14 ...metadata,
15 attachedAt: new Date(),
16 interactions: 0
17 });
18 }
19
20 getMetadata(element) {
21 return this.elements.get(element);
22 }
23
24 incrementInteractions(element) {
25 const metadata = this.elements.get(element);
26 if (metadata) {
27 metadata.interactions++;
28 }
29 }
30
31 // Create relationships between elements
32 addRelationship(element1, element2, type) {
33 if (!this.relationships.has(element1)) {
34 this.relationships.set(element1, new Map());
35 }
36 this.relationships.get(element1).set(element2, type);
37 }
38
39 getRelationships(element) {
40 return this.relationships.get(element) || new Map();
41 }
42
43 // WeakMap doesn't have size or iteration methods
44 // This is intentional - you can't enumerate keys
45}
46
47const manager = new ElementManager();
48
49// Create some DOM elements
50let button = document.createElement('button');
51let input = document.createElement('input');
52
53manager.attachMetadata(button, {
54 component: 'SubmitButton',
55 theme: 'primary'
56});
57
58manager.attachMetadata(input, {
59 component: 'TextInput',
60 validation: 'required'
61});
62
63manager.addRelationship(button, input, 'submits');
64
65console.log('Button metadata:', manager.getMetadata(button));
66console.log('Input metadata:', manager.getMetadata(input));
67
68// When elements are no longer referenced, WeakMap entries are automatically cleaned up
69button = null; // The metadata for this button can now be garbage collected
70input = null; // Same for input
71
72// WeakSet: Values must be objects and don't prevent garbage collection
73const processedObjects = new WeakSet();
74const observedObjects = new WeakSet();
75
76class ObjectProcessor {
77 constructor() {
78 this.processed = new WeakSet();
79 this.observers = new WeakMap(); // object -> Set of observer functions
80 }
81
82 process(obj) {
83 if (this.processed.has(obj)) {
84 console.log('Object already processed');
85 return;
86 }
87
88 // Simulate processing
89 console.log('Processing object:', obj);
90 this.processed.add(obj);
91
92 // Notify observers
93 this.notifyObservers(obj, 'processed');
94 }
95
96 isProcessed(obj) {
97 return this.processed.has(obj);
98 }
99
100 addObserver(obj, callback) {
101 if (!this.observers.has(obj)) {
102 this.observers.set(obj, new Set());
103 }
104 this.observers.get(obj).add(callback);
105 }
106
107 removeObserver(obj, callback) {
108 const observers = this.observers.get(obj);
109 if (observers) {
110 observers.delete(callback);
111 if (observers.size === 0) {
112 this.observers.delete(obj);
113 }
114 }
115 }
116
117 notifyObservers(obj, event) {
118 const observers = this.observers.get(obj);
119 if (observers) {
120 observers.forEach(callback => callback(event, obj));
121 }
122 }
123}
124
125const processor = new ObjectProcessor();
126
127let data1 = { id: 1, value: 'test' };
128let data2 = { id: 2, value: 'another' };
129
130// Add observers
131processor.addObserver(data1, (event, obj) => {
132 console.log(`Observer: Object ${obj.id} was ${event}`);
133});
134
135processor.process(data1);
136processor.process(data1); // Won't process again
137console.log('Is data1 processed?', processor.isProcessed(data1));
138
139// When objects are no longer referenced, WeakSet/WeakMap entries are cleaned up
140data1 = null;
141data2 = null;Advanced Pattern: Registry System:
1// Complex registry system combining all data structures
2class AdvancedRegistry {
3 constructor() {
4 // Use Symbol for private registry access
5 this._registryKey = Symbol('registryAccess');
6
7 // Main registry: Map for ordered, iterable storage
8 this.registry = new Map();
9
10 // Unique items: Set for fast membership testing
11 this.uniqueItems = new Set();
12
13 // Metadata: WeakMap for garbage-collection-friendly metadata
14 this.metadata = new WeakMap();
15
16 // Event subscribers: Map of event names to Sets of callbacks
17 this.subscribers = new Map();
18
19 // Statistics
20 this.stats = new Map([
21 ['registrations', 0],
22 ['unregistrations', 0],
23 ['lookups', 0]
24 ]);
25 }
26
27 register(id, item, metadata = {}) {
28 // Check if already registered
29 if (this.registry.has(id)) {
30 throw new Error(`Item with id '${id}' already registered`);
31 }
32
33 // Ensure item uniqueness
34 if (this.uniqueItems.has(item)) {
35 throw new Error('Item already registered with different ID');
36 }
37
38 // Register the item
39 this.registry.set(id, item);
40 this.uniqueItems.add(item);
41
42 // Add metadata if item is an object
43 if (typeof item === 'object' && item !== null) {
44 this.metadata.set(item, {
45 ...metadata,
46 registeredAt: new Date(),
47 id,
48 accessCount: 0
49 });
50 }
51
52 // Update stats
53 this.stats.set('registrations', this.stats.get('registrations') + 1);
54
55 // Emit registration event
56 this.emit('register', { id, item, metadata });
57
58 return this;
59 }
60
61 unregister(id) {
62 const item = this.registry.get(id);
63 if (!item) {
64 return false;
65 }
66
67 // Remove from all collections
68 this.registry.delete(id);
69 this.uniqueItems.delete(item);
70
71 // Metadata will be automatically cleaned up by WeakMap
72 // when item is no longer referenced elsewhere
73
74 // Update stats
75 this.stats.set('unregistrations', this.stats.get('unregistrations') + 1);
76
77 // Emit unregistration event
78 this.emit('unregister', { id, item });
79
80 return true;
81 }
82
83 get(id) {
84 this.stats.set('lookups', this.stats.get('lookups') + 1);
85
86 const item = this.registry.get(id);
87 if (item && typeof item === 'object' && item !== null) {
88 const meta = this.metadata.get(item);
89 if (meta) {
90 meta.accessCount++;
91 meta.lastAccessed = new Date();
92 }
93 }
94
95 return item;
96 }
97
98 has(id) {
99 return this.registry.has(id);
100 }
101
102 isRegistered(item) {
103 return this.uniqueItems.has(item);
104 }
105
106 getMetadata(item) {
107 return this.metadata.get(item);
108 }
109
110 getAllIds() {
111 return [...this.registry.keys()];
112 }
113
114 getAllItems() {
115 return [...this.registry.values()];
116 }
117
118 getAll() {
119 return [...this.registry.entries()];
120 }
121
122 size() {
123 return this.registry.size;
124 }
125
126 getStats() {
127 return new Map(this.stats);
128 }
129
130 // Event system
131 on(event, callback) {
132 if (!this.subscribers.has(event)) {
133 this.subscribers.set(event, new Set());
134 }
135 this.subscribers.get(event).add(callback);
136 }
137
138 off(event, callback) {
139 const callbacks = this.subscribers.get(event);
140 if (callbacks) {
141 callbacks.delete(callback);
142 if (callbacks.size === 0) {
143 this.subscribers.delete(event);
144 }
145 }
146 }
147
148 emit(event, data) {
149 const callbacks = this.subscribers.get(event);
150 if (callbacks) {
151 callbacks.forEach(callback => {
152 try {
153 callback(data);
154 } catch (error) {
155 console.error('Registry event callback error:', error);
156 }
157 });
158 }
159 }
160
161 // Batch operations
162 registerBatch(items) {
163 const results = [];
164 for (const [id, item, metadata] of items) {
165 try {
166 this.register(id, item, metadata);
167 results.push({ id, success: true });
168 } catch (error) {
169 results.push({ id, success: false, error: error.message });
170 }
171 }
172 return results;
173 }
174
175 // Search functionality
176 search(predicate) {
177 const results = [];
178 for (const [id, item] of this.registry) {
179 if (predicate(item, id)) {
180 results.push({ id, item });
181 }
182 }
183 return results;
184 }
185
186 // Clear all data
187 clear() {
188 const count = this.registry.size;
189 this.registry.clear();
190 this.uniqueItems.clear();
191 // WeakMap entries will be cleaned up automatically
192
193 this.emit('clear', { clearedCount: count });
194 return count;
195 }
196}
197
198// Usage example
199const registry = new AdvancedRegistry();
200
201// Subscribe to events
202registry.on('register', (data) => {
203 console.log(`Registered: ${data.id}`);
204});
205
206// Register various types of items
207const obj1 = { name: 'Object 1', type: 'data' };
208const obj2 = { name: 'Object 2', type: 'config' };
209const func1 = () => console.log('Function 1');
210
211registry.register('obj1', obj1, { category: 'data', priority: 'high' });
212registry.register('obj2', obj2, { category: 'config', priority: 'normal' });
213registry.register('func1', func1, { category: 'function', priority: 'low' });
214registry.register('string1', 'Hello World');
215registry.register('number1', 42);
216
217console.log('Registry size:', registry.size());
218console.log('All IDs:', registry.getAllIds());
219
220// Access items and check metadata
221const retrievedObj = registry.get('obj1');
222console.log('Retrieved object:', retrievedObj);
223console.log('Object metadata:', registry.getMetadata(retrievedObj));
224
225// Search functionality
226const dataItems = registry.search((item, id) => {
227 const meta = typeof item === 'object' ? registry.getMetadata(item) : null;
228 return meta && meta.category === 'data';
229});
230console.log('Data items:', dataItems);
231
232// Statistics
233console.log('Registry stats:', registry.getStats());These advanced patterns demonstrate how Symbols, Sets, Maps, and their weak variants can be combined to create powerful, memory-efficient data structures for complex applications.
Create a sophisticated state management system that demonstrates Symbols, Sets, Maps, and WeakMaps working together to provide private state, event handling, and efficient data management.
1// === Complete Application State Management System ===
2// Demonstrates advanced usage of Symbols, Sets, Maps, and WeakMaps
3
4// Private symbols for internal state management
5const STATE_KEY = Symbol('appState');
6const LISTENERS_KEY = Symbol('listeners');
7const MIDDLEWARE_KEY = Symbol('middleware');
8const HISTORY_KEY = Symbol('history');
9const SUBSCRIPTIONS_KEY = Symbol('subscriptions');
10
11// WeakMaps for garbage collection-friendly metadata
12const componentMetadata = new WeakMap();
13const stateChangeMetadata = new WeakMap();
14const performanceMetrics = new WeakMap();
15
16// Global registries using Maps for efficient lookup and ordered iteration
17const globalEventBus = new Map();
18const moduleRegistry = new Map();
19const pluginRegistry = new Set();
20
21/**
22 * Core State Management Class
23 * Uses Symbols for private properties and Maps/Sets for organized data storage
24 */
25class StateManager {
26 constructor(initialState = {}) {
27 // Private state using symbols
28 this[STATE_KEY] = new Map(Object.entries(initialState));
29 this[LISTENERS_KEY] = new Map(); // path -> Set of listeners
30 this[MIDDLEWARE_KEY] = new Set(); // Set of middleware functions
31 this[HISTORY_KEY] = []; // Array of state snapshots
32 this[SUBSCRIPTIONS_KEY] = new WeakMap(); // component -> subscription info
33
34 // Public properties
35 this.id = `state_${Math.random().toString(36).substr(2, 9)}`;
36 this.createdAt = new Date();
37
38 // Performance tracking
39 this.metrics = new Map([
40 ['stateChanges', 0],
41 ['listenerInvocations', 0],
42 ['middlewareExecutions', 0]
43 ]);
44
45 // Initialize
46 this.saveSnapshot('initial');
47 }
48
49 // State manipulation methods
50 setState(path, value) {
51 const oldValue = this.getState(path);
52
53 // Run middleware
54 const context = {
55 path,
56 oldValue,
57 newValue: value,
58 timestamp: new Date(),
59 stateManager: this
60 };
61
62 // Execute middleware chain
63 for (const middleware of this[MIDDLEWARE_KEY]) {
64 try {
65 const result = middleware(context);
66 if (result === false) {
67 // Middleware can cancel the state change
68 return false;
69 }
70 // Middleware can modify the value
71 if (result !== undefined) {
72 context.newValue = result;
73 }
74 } catch (error) {
75 console.error('Middleware error:', error);
76 }
77 }
78
79 // Update state
80 this[STATE_KEY].set(path, context.newValue);
81 this.metrics.set('stateChanges', this.metrics.get('stateChanges') + 1);
82
83 // Create change metadata
84 const changeInfo = {
85 path,
86 oldValue,
87 newValue: context.newValue,
88 timestamp: context.timestamp,
89 changeId: `change_${this.metrics.get('stateChanges')}`
90 };
91
92 // Store change metadata using WeakMap (will be cleaned up when changeInfo is released)
93 stateChangeMetadata.set(changeInfo, {
94 stackTrace: new Error().stack,
95 performanceEntry: performance.now()
96 });
97
98 // Notify listeners
99 this.notifyListeners(path, changeInfo);
100
101 // Save snapshot if significant change
102 if (this.isSignificantChange(oldValue, context.newValue)) {
103 this.saveSnapshot(`change_${this.metrics.get('stateChanges')}`);
104 }
105
106 return true;
107 }
108
109 getState(path) {
110 if (path === undefined) {
111 // Return entire state as plain object
112 return Object.fromEntries(this[STATE_KEY]);
113 }
114 return this[STATE_KEY].get(path);
115 }
116
117 hasState(path) {
118 return this[STATE_KEY].has(path);
119 }
120
121 deleteState(path) {
122 const existed = this[STATE_KEY].delete(path);
123 if (existed) {
124 this.notifyListeners(path, {
125 path,
126 oldValue: this.getState(path),
127 newValue: undefined,
128 deleted: true
129 });
130 }
131 return existed;
132 }
133
134 // Listener management
135 subscribe(path, listener, options = {}) {
136 if (!this[LISTENERS_KEY].has(path)) {
137 this[LISTENERS_KEY].set(path, new Set());
138 }
139
140 const enhancedListener = {
141 callback: listener,
142 options,
143 id: `listener_${Math.random().toString(36).substr(2, 9)}`,
144 subscribedAt: new Date(),
145 callCount: 0
146 };
147
148 this[LISTENERS_KEY].get(path).add(enhancedListener);
149
150 // Return unsubscribe function
151 return () => {
152 const listeners = this[LISTENERS_KEY].get(path);
153 if (listeners) {
154 listeners.delete(enhancedListener);
155 if (listeners.size === 0) {
156 this[LISTENERS_KEY].delete(path);
157 }
158 }
159 };
160 }
161
162 notifyListeners(path, changeInfo) {
163 const listeners = this[LISTENERS_KEY].get(path);
164 if (!listeners) return;
165
166 for (const listener of listeners) {
167 try {
168 // Check if listener should be called based on options
169 if (this.shouldCallListener(listener, changeInfo)) {
170 listener.callback(changeInfo, this.getState(path));
171 listener.callCount++;
172 this.metrics.set('listenerInvocations',
173 this.metrics.get('listenerInvocations') + 1);
174 }
175 } catch (error) {
176 console.error('Listener error:', error);
177 }
178 }
179 }
180
181 shouldCallListener(listener, changeInfo) {
182 const { options } = listener;
183
184 // Check if listener has a filter
185 if (options.filter && !options.filter(changeInfo)) {
186 return false;
187 }
188
189 // Check debounce
190 if (options.debounce) {
191 const now = Date.now();
192 if (listener.lastCalled && (now - listener.lastCalled) < options.debounce) {
193 return false;
194 }
195 listener.lastCalled = now;
196 }
197
198 return true;
199 }
200
201 // Middleware management
202 addMiddleware(middleware) {
203 this[MIDDLEWARE_KEY].add(middleware);
204 return () => this[MIDDLEWARE_KEY].delete(middleware);
205 }
206
207 // History and snapshots
208 saveSnapshot(label) {
209 const snapshot = {
210 label,
211 state: Object.fromEntries(this[STATE_KEY]),
212 timestamp: new Date(),
213 metrics: new Map(this.metrics)
214 };
215
216 this[HISTORY_KEY].push(snapshot);
217
218 // Keep only last 50 snapshots
219 if (this[HISTORY_KEY].length > 50) {
220 this[HISTORY_KEY].shift();
221 }
222 }
223
224 getHistory() {
225 return [...this[HISTORY_KEY]];
226 }
227
228 restoreSnapshot(index) {
229 const snapshot = this[HISTORY_KEY][index];
230 if (!snapshot) {
231 throw new Error('Snapshot not found');
232 }
233
234 this[STATE_KEY].clear();
235 for (const [key, value] of Object.entries(snapshot.state)) {
236 this[STATE_KEY].set(key, value);
237 }
238
239 this.saveSnapshot(`restored_${snapshot.label}`);
240
241 // Notify all listeners of the restore
242 for (const path of this[STATE_KEY].keys()) {
243 this.notifyListeners(path, {
244 path,
245 oldValue: undefined,
246 newValue: this.getState(path),
247 restored: true
248 });
249 }
250 }
251
252 isSignificantChange(oldValue, newValue) {
253 // Simple heuristic for significant changes
254 if (typeof oldValue !== typeof newValue) return true;
255 if (typeof oldValue === 'object') {
256 return JSON.stringify(oldValue) !== JSON.stringify(newValue);
257 }
258 return oldValue !== newValue;
259 }
260
261 // Component integration
262 bindComponent(component, bindings = {}) {
263 const subscriptions = new Map();
264
265 // Create subscriptions for each binding
266 for (const [componentProp, statePath] of Object.entries(bindings)) {
267 const unsubscribe = this.subscribe(statePath, (changeInfo, newValue) => {
268 if (component[componentProp] !== newValue) {
269 component[componentProp] = newValue;
270
271 // Trigger component update if it has an update method
272 if (typeof component.update === 'function') {
273 component.update();
274 }
275 }
276 });
277
278 subscriptions.set(componentProp, {
279 statePath,
280 unsubscribe,
281 boundAt: new Date()
282 });
283
284 // Set initial value
285 component[componentProp] = this.getState(statePath);
286 }
287
288 // Store subscription info in WeakMap
289 this[SUBSCRIPTIONS_KEY].set(component, {
290 subscriptions,
291 boundAt: new Date(),
292 stateManager: this
293 });
294
295 // Store component metadata
296 componentMetadata.set(component, {
297 id: `component_${Math.random().toString(36).substr(2, 9)}`,
298 bindings: Object.keys(bindings),
299 boundTo: this.id,
300 bindingCount: subscriptions.size
301 });
302
303 // Return unbind function
304 return () => this.unbindComponent(component);
305 }
306
307 unbindComponent(component) {
308 const subscriptionInfo = this[SUBSCRIPTIONS_KEY].get(component);
309 if (!subscriptionInfo) return false;
310
311 // Unsubscribe all
312 for (const [prop, info] of subscriptionInfo.subscriptions) {
313 info.unsubscribe();
314 }
315
316 // Clean up
317 this[SUBSCRIPTIONS_KEY].delete(component);
318 // componentMetadata will be cleaned up automatically when component is GC'd
319
320 return true;
321 }
322
323 // Utility methods
324 getAllPaths() {
325 return [...this[STATE_KEY].keys()];
326 }
327
328 getListenerCount(path) {
329 const listeners = this[LISTENERS_KEY].get(path);
330 return listeners ? listeners.size : 0;
331 }
332
333 getTotalListeners() {
334 let total = 0;
335 for (const listeners of this[LISTENERS_KEY].values()) {
336 total += listeners.size;
337 }
338 return total;
339 }
340
341 getMetrics() {
342 return new Map(this.metrics);
343 }
344
345 // Debug and introspection
346 getInternalState() {
347 return {
348 stateSize: this[STATE_KEY].size,
349 listenerPaths: this[LISTENERS_KEY].size,
350 totalListeners: this.getTotalListeners(),
351 middlewareCount: this[MIDDLEWARE_KEY].size,
352 historyLength: this[HISTORY_KEY].length,
353 metrics: this.getMetrics()
354 };
355 }
356}
357
358/**
359 * Plugin System using Sets and Maps
360 */
361class PluginManager {
362 constructor() {
363 this.plugins = new Set();
364 this.pluginMetadata = new Map();
365 this.hooks = new Map(); // hookName -> Set of callbacks
366 this.loadedPlugins = new Map(); // pluginName -> plugin instance
367 }
368
369 register(plugin) {
370 if (this.plugins.has(plugin)) {
371 throw new Error('Plugin already registered');
372 }
373
374 this.plugins.add(plugin);
375
376 const metadata = {
377 name: plugin.name || 'anonymous',
378 version: plugin.version || '1.0.0',
379 registeredAt: new Date(),
380 hooks: new Set(),
381 id: `plugin_${Math.random().toString(36).substr(2, 9)}`
382 };
383
384 this.pluginMetadata.set(plugin, metadata);
385
386 // Auto-register hooks if plugin has them
387 if (plugin.hooks) {
388 for (const [hookName, callback] of Object.entries(plugin.hooks)) {
389 this.addHook(hookName, callback, plugin);
390 metadata.hooks.add(hookName);
391 }
392 }
393
394 // Initialize plugin if it has init method
395 if (typeof plugin.init === 'function') {
396 try {
397 plugin.init(this);
398 this.loadedPlugins.set(metadata.name, plugin);
399 } catch (error) {
400 console.error(`Plugin ${metadata.name} initialization failed:`, error);
401 }
402 }
403
404 return metadata.id;
405 }
406
407 unregister(plugin) {
408 if (!this.plugins.has(plugin)) {
409 return false;
410 }
411
412 const metadata = this.pluginMetadata.get(plugin);
413
414 // Remove all hooks registered by this plugin
415 for (const hookName of metadata.hooks) {
416 this.removeHook(hookName, plugin);
417 }
418
419 // Clean up
420 this.plugins.delete(plugin);
421 this.pluginMetadata.delete(plugin);
422 this.loadedPlugins.delete(metadata.name);
423
424 // Cleanup plugin if it has cleanup method
425 if (typeof plugin.cleanup === 'function') {
426 try {
427 plugin.cleanup();
428 } catch (error) {
429 console.error(`Plugin ${metadata.name} cleanup failed:`, error);
430 }
431 }
432
433 return true;
434 }
435
436 addHook(hookName, callback, plugin = null) {
437 if (!this.hooks.has(hookName)) {
438 this.hooks.set(hookName, new Set());
439 }
440
441 const hookInfo = {
442 callback,
443 plugin,
444 addedAt: new Date()
445 };
446
447 this.hooks.get(hookName).add(hookInfo);
448 }
449
450 removeHook(hookName, plugin) {
451 const hooks = this.hooks.get(hookName);
452 if (!hooks) return false;
453
454 for (const hookInfo of hooks) {
455 if (hookInfo.plugin === plugin) {
456 hooks.delete(hookInfo);
457 break;
458 }
459 }
460
461 if (hooks.size === 0) {
462 this.hooks.delete(hookName);
463 }
464
465 return true;
466 }
467
468 executeHook(hookName, ...args) {
469 const hooks = this.hooks.get(hookName);
470 if (!hooks) return [];
471
472 const results = [];
473
474 for (const hookInfo of hooks) {
475 try {
476 const result = hookInfo.callback(...args);
477 results.push({
478 plugin: hookInfo.plugin ? this.pluginMetadata.get(hookInfo.plugin).name : 'anonymous',
479 result
480 });
481 } catch (error) {
482 console.error(`Hook ${hookName} error:, error`);
483 results.push({
484 plugin: hookInfo.plugin ? this.pluginMetadata.get(hookInfo.plugin).name : 'anonymous',
485 error: error.message
486 });
487 }
488 }
489
490 return results;
491 }
492
493 getPlugin(name) {
494 return this.loadedPlugins.get(name);
495 }
496
497 listPlugins() {
498 return Array.from(this.plugins).map(plugin => {
499 const metadata = this.pluginMetadata.get(plugin);
500 return {
501 name: metadata.name,
502 version: metadata.version,
503 hooks: [...metadata.hooks],
504 registeredAt: metadata.registeredAt
505 };
506 });
507 }
508}
509
510/**
511 * Component Base Class
512 * Demonstrates integration with state management and plugins
513 */
514class Component {
515 constructor(name) {
516 this.name = name;
517 this.id = `comp_${Math.random().toString(36).substr(2, 9)}`;
518 this.state = new Map();
519 this.props = new Map();
520 this.events = new Map(); // eventName -> Set of handlers
521 this.mounted = false;
522 this.updateCount = 0;
523 }
524
525 // Event system
526 on(event, handler) {
527 if (!this.events.has(event)) {
528 this.events.set(event, new Set());
529 }
530 this.events.get(event).add(handler);
531
532 return () => this.events.get(event).delete(handler);
533 }
534
535 emit(event, data) {
536 const handlers = this.events.get(event);
537 if (handlers) {
538 handlers.forEach(handler => {
539 try {
540 handler(data);
541 } catch (error) {
542 console.error(`Event handler error in ${this.name}:`, error);
543 }
544 });
545 }
546 }
547
548 // State management
549 setState(key, value) {
550 const oldValue = this.state.get(key);
551 this.state.set(key, value);
552
553 this.emit('stateChange', { key, oldValue, newValue: value });
554 this.update();
555 }
556
557 getState(key) {
558 return this.state.get(key);
559 }
560
561 // Lifecycle methods
562 mount() {
563 if (this.mounted) return;
564
565 this.mounted = true;
566 this.emit('mount');
567
568 if (typeof this.onMount === 'function') {
569 this.onMount();
570 }
571 }
572
573 unmount() {
574 if (!this.mounted) return;
575
576 this.mounted = false;
577 this.emit('unmount');
578
579 if (typeof this.onUnmount === 'function') {
580 this.onUnmount();
581 }
582
583 // Cleanup events
584 this.events.clear();
585 }
586
587 update() {
588 if (!this.mounted) return;
589
590 this.updateCount++;
591 this.emit('update', { updateCount: this.updateCount });
592
593 if (typeof this.onUpdate === 'function') {
594 this.onUpdate();
595 }
596 }
597
598 // Debug info
599 getDebugInfo() {
600 return {
601 name: this.name,
602 id: this.id,
603 mounted: this.mounted,
604 updateCount: this.updateCount,
605 stateKeys: [...this.state.keys()],
606 eventTypes: [...this.events.keys()],
607 metadata: componentMetadata.get(this)
608 };
609 }
610}
611
612/**
613 * Application Demo
614 */
615function createDemoApplication() {
616 console.log('๐ Creating Advanced State Management Demo');
617 console.log('==========================================');
618
619 // Create state manager
620 const stateManager = new StateManager({
621 user: { name: 'John Doe', email: '[email protected]' },
622 theme: 'light',
623 notifications: [],
624 settings: { autoSave: true, language: 'en' }
625 });
626
627 // Add middleware
628 stateManager.addMiddleware((context) => {
629 console.log(`๐ง Middleware: ${context.path} changed from ${context.oldValue} to ${context.newValue}`);
630
631 // Example: Validate theme changes
632 if (context.path === 'theme' && !['light', 'dark'].includes(context.newValue)) {
633 console.warn('Invalid theme value, using default');
634 return 'light';
635 }
636 });
637
638 // Create plugin manager
639 const pluginManager = new PluginManager();
640
641 // Create sample plugin
642 const analyticsPlugin = {
643 name: 'analytics',
644 version: '1.0.0',
645 init(pluginManager) {
646 console.log('๐ Analytics plugin initialized');
647 },
648 hooks: {
649 stateChange: (changeInfo) => {
650 console.log(`๐ Analytics: State change tracked - ${changeInfo.path}`);
651 }
652 }
653 };
654
655 pluginManager.register(analyticsPlugin);
656
657 // Create components
658 class UserProfile extends Component {
659 onMount() {
660 console.log(`๐ค UserProfile component mounted: ${this.name}`);
661 }
662
663 onUpdate() {
664 console.log(`๐ค UserProfile updated (count: ${this.updateCount})`);
665 }
666 }
667
668 class ThemeSelector extends Component {
669 onMount() {
670 console.log(`๐จ ThemeSelector component mounted: ${this.name}`);
671 }
672
673 onUpdate() {
674 const theme = this.getState('theme');
675 console.log(`๐จ Theme changed to: ${theme}`);
676 }
677 }
678
679 // Create component instances
680 const userProfile = new UserProfile('UserProfile');
681 const themeSelector = new ThemeSelector('ThemeSelector');
682
683 // Bind components to state
684 const unbindUser = stateManager.bindComponent(userProfile, {
685 userData: 'user',
686 notifications: 'notifications'
687 });
688
689 const unbindTheme = stateManager.bindComponent(themeSelector, {
690 theme: 'theme'
691 });
692
693 // Mount components
694 userProfile.mount();
695 themeSelector.mount();
696
697 // Subscribe to state changes
698 const unsubscribeTheme = stateManager.subscribe('theme', (changeInfo) => {
699 pluginManager.executeHook('stateChange', changeInfo);
700 });
701
702 console.log('\n๐ Initial State:');
703 console.log('State Manager:', stateManager.getInternalState());
704 console.log('Plugins:', pluginManager.listPlugins());
705
706 // Demonstrate state changes
707 console.log('\n๐ Making State Changes:');
708 stateManager.setState('theme', 'dark');
709 stateManager.setState('user', {
710 ...stateManager.getState('user'),
711 lastLogin: new Date()
712 });
713
714 // Add notification
715 const notifications = stateManager.getState('notifications') || [];
716 stateManager.setState('notifications', [
717 ...notifications,
718 { id: 1, message: 'Welcome back!', type: 'info', timestamp: new Date() }
719 ]);
720
721 // Component interaction
722 userProfile.setState('customProp', 'custom value');
723 themeSelector.emit('themeChanged', { theme: stateManager.getState('theme') });
724
725 console.log('\n๐ Performance Metrics:');
726 console.log('State Manager Metrics:', stateManager.getMetrics());
727
728 console.log('\n๐ Component Debug Info:');
729 console.log('User Profile:', userProfile.getDebugInfo());
730 console.log('Theme Selector:', themeSelector.getDebugInfo());
731
732 console.log('\n๐ State History:');
733 stateManager.getHistory().forEach((snapshot, index) => {
734 console.log(` ${index}: ${snapshot.label} at ${snapshot.timestamp.toLocaleTimeString()}`);
735 });
736
737 // Cleanup demonstration
738 setTimeout(() => {
739 console.log('\n๐งน Cleanup Demo:');
740
741 // Unbind components
742 unbindUser();
743 unbindTheme();
744 unsubscribeTheme();
745
746 // Unmount components
747 userProfile.unmount();
748 themeSelector.unmount();
749
750 // Unregister plugins
751 pluginManager.unregister(analyticsPlugin);
752
753 console.log('โ
Cleanup completed');
754 console.log('Final State Manager State:', stateManager.getInternalState());
755
756 }, 3000);
757
758 return {
759 stateManager,
760 pluginManager,
761 components: { userProfile, themeSelector }
762 };
763}
764
765// Auto-start demo
766const demoApp = createDemoApplication();
767
768// Make available globally for inspection
769window.demoApp = demoApp;
770
771console.log('\n๐ Advanced State Management Demo is running!');
772console.log('Check window.demoApp for inspection');
773
774/*
775๐ฏ Key Features Demonstrated:
776
7771. **Symbols for Privacy**:
778 - Private properties that can't be easily accessed externally
779 - Unique identifiers that avoid naming collisions
780
7812. **Maps for Efficient Storage**:
782 - State storage with any type of keys
783 - Ordered iteration and efficient lookups
784 - Event listener management with organized storage
785
7863. **Sets for Unique Collections**:
787 - Middleware management without duplicates
788 - Event handler sets for efficient management
789 - Plugin registration with uniqueness guarantees
790
7914. **WeakMap/WeakSet for Memory Management**:
792 - Metadata storage that doesn't prevent garbage collection
793 - Component binding info that cleans up automatically
794 - Performance metadata for temporary objects
795
7965. **Combined Patterns**:
797 - Registry systems using multiple data structures
798 - Event systems with efficient storage and lookup
799 - Plugin architecture with organized metadata
800
8016. **Real-world Applications**:
802 - State management with history and snapshots
803 - Component binding with automatic cleanup
804 - Plugin system with hooks and lifecycle management
805 - Performance tracking and debugging tools
806
807This demonstrates how ES6's advanced data structures can be
808combined to create sophisticated, memory-efficient systems
809for complex application architectures.
810*/Your progress is saved in this browser, on this device only.