Skillversitas

50. Map & Set: Collection Data Structures in JavaScript

Introduction

Map and Set are collection data structures introduced in ES6. A Map stores key-value pairs where the key can be of any data type. A Set stores unique values without duplicates. Both offer O(1) lookup performance, which is far better than arrays for search operations.

For IT students working with campus data, Map and Set are very useful for caching student data, tracking enrollment, and manipulating data collections efficiently.

Why Are Map and Set Important?

  • Plain objects can only use string/Symbol as keys
  • Arrays have to do a linear search to check for a value's existence
  • Maps preserve insertion order, are iterable, and have O(1) lookups
  • Sets automatically remove duplicates with O(1) has()

Map

Creating and Filling a Map

// Create an empty Map
const studentMap = new Map();

// Add data with set()
studentMap.set('1234567890', { name: 'Mike', majorCode: 'CS' });
studentMap.set('0987654321', { name: 'John', majorCode: 'IS' });

// Create a Map from an array of pairs
const data = [
    ['1234567890', 'Mike'],
    ['0987654321', 'John'],
    ['1122334455', 'Anna']
];
const nameMap = new Map(data);

// Create a Map from an Object
const obj = { a: 1, b: 2, c: 3 };
const objMap = new Map(Object.entries(obj));

// Clone
const cloneMap = new Map(studentMap);

Basic Map Operations

const studentMap = new Map();

// set() - add/update data (chainable)
studentMap.set('studentId1', { name: 'Mike', gpa: 3.5 });
studentMap.set('studentId2', { name: 'John', gpa: 3.8 });
studentMap.set('studentId3', { name: 'Anna', gpa: 3.2 });

// Object as a key
const keyObj = { id: 1 };
studentMap.set(keyObj, 'Data for key object');

// get() - retrieve data
const mike = studentMap.get('studentId1');
console.log(mike); // { name: 'Mike', gpa: 3.5 }

// has() - check for the existence of a key
console.log(studentMap.has('studentId1')); // true
console.log(studentMap.has('studentId999')); // false

// delete() - remove data
studentMap.delete('studentId3');

// size - number of entries
console.log(studentMap.size); // 2

// clear() - remove all data
studentMap.clear();

Iterating a Map

const studentMap = new Map();
studentMap.set('studentId1', { name: 'Mike', gpa: 3.5 });
studentMap.set('studentId2', { name: 'John', gpa: 3.8 });
studentMap.set('studentId3', { name: 'Anna', gpa: 3.2 });

// forEach
studentMap.forEach((value, key) => {
    console.log(`${key}: ${value.name}`);
});

// for...of
for (const [studentId, data] of studentMap) {
    console.log(`${studentId}: ${data.name} - GPA: ${data.gpa}`);
}

// entries()
for (const [studentId, data] of studentMap.entries()) {
    console.log(`${studentId}: ${data.name}`);
}

// keys()
for (const studentId of studentMap.keys()) {
    console.log(`StudentID: ${studentId}`);
}

// values()
for (const data of studentMap.values()) {
    console.log(`Name: ${data.name}`);
}

// Convert to an array
const entriesArray = [...studentMap.entries()];
const keysArray = [...studentMap.keys()];
const valuesArray = [...studentMap.values()];

// Sort a Map by GPA (GPA)
const sortedMap = new Map(
    [...studentMap.entries()].sort((a, b) => b[1].gpa - a[1].gpa)
);

Set

Creating and Filling a Set

// Create an empty Set
const uniqueStudentID = new Set();

// Add data
uniqueStudentID.add('1234567890');
uniqueStudentID.add('0987654321');
uniqueStudentID.add('1234567890'); // Duplicate, not added
uniqueStudentID.add('1122334455');

console.log(uniqueStudentID.size); // 3

// Create a Set from an array
const numbers = [1, 2, 3, 3, 4, 4, 5];
const uniqueNumbers = new Set(numbers);
console.log([...uniqueNumbers]); // [1, 2, 3, 4, 5]

// Create a Set from a string
const chars = new Set('hello');
console.log([...chars]); // ['h', 'e', 'l', 'o']

Basic Set Operations

const majorCodeSet = new Set(['CS', 'IS', 'CE', 'IS']); // SI is a duplicate

// add() - add data
majorCodeSet.add('MMA');
majorCodeSet.add('CS'); // Duplicate, not added

// has() - check for existence
console.log(majorCodeSet.has('CS')); // true
console.log(majorCodeSet.has('DES')); // false

// delete() - remove data
majorCodeSet.delete('CE');

// size
console.log(majorCodeSet.size); // 3

// clear()
// majorCodeSet.clear();

Advanced Set Operations: Union, Intersection, Difference

const setA = new Set(['CS', 'IS', 'CE']);
const setB = new Set(['IS', 'MMA', 'DES']);

// Union - combine all elements
const union = new Set([...setA, ...setB]);
console.log([...union]); // ['CS', 'IS', 'CE', 'MMA', 'DES']

// Intersection - elements present in both sets
const intersection = new Set(
    [...setA].filter(x => setB.has(x))
);
console.log([...intersection]); // ['IS']

// Difference (A - B) - elements in A but not in B
const difference = new Set(
    [...setA].filter(x => !setB.has(x))
);
console.log([...difference]); // ['CS', 'CE']

// Symmetric Difference - elements in A or B but not both
const symmetricDiff = new Set(
    [...setA].filter(x => !setB.has(x)).concat(
        [...setB].filter(x => !setA.has(x))
    )
);
console.log([...symmetricDiff]); // ['CS', 'CE', 'MMA', 'DES']

// Subset check
function isSubset(subset, superset) {
    for (const item of subset) {
        if (!superset.has(item)) return false;
    }
    return true;
}

console.log(isSubset(new Set(['CS']), setA)); // true
console.log(isSubset(new Set(['MMA']), setA)); // false

WeakMap and WeakSet

// WeakMap - keys must be objects, garbage collectible
const studentData = new WeakMap();

class Student {
    constructor(name, studentId) {
        this.name = name;
        this.studentId = studentId;
    }
}

const mike = new Student('Mike', '001');
const john = new Student('John', '002');

// Store related data in a WeakMap
studentData.set(mike, { enrollDate: '2024-01-15', status: 'active' });
studentData.set(john, { enrollDate: '2024-02-01', status: 'active' });

console.log(studentData.get(mike));
// { enrollDate: '2024-01-15', status: 'active' }

// When mike = null, the data in the WeakMap is automatically garbage collected
// WeakMap does not have: size, keys(), values(), entries(), forEach, iterate

// WeakSet - only stores objects, cannot be iterated
const activeStudents = new WeakSet();
const student1 = { name: 'Mike' };
const student2 = { name: 'John' };

activeStudents.add(student1);
activeStudents.add(student2);

console.log(activeStudents.has(student1)); // true

// When student1 = null, it is automatically removed from the WeakSet

Real-World Example: Caching Student Data with a Map

class StudentCache {
    #cache;
    #maxSize;
    #accessOrder;

    constructor(maxSize = 100) {
        this.#cache = new Map();
        this.#maxSize = maxSize;
        this.#accessOrder = [];
    }

    get(studentId) {
        if (this.#cache.has(studentId)) {
            // Update access order (LRU)
            this.#updateAccess(studentId);
            return this.#cache.get(studentId);
        }
        return null;
    }

    set(studentId, data) {
        // If the cache is full, remove the least recently accessed entry
        if (this.#cache.size >= this.#maxSize && !this.#cache.has(studentId)) {
            const oldest = this.#accessOrder.shift();
            this.#cache.delete(oldest);
        }

        this.#cache.set(studentId, data);
        this.#updateAccess(studentId);
    }

    has(studentId) {
        return this.#cache.has(studentId);
    }

    delete(studentId) {
        this.#accessOrder = this.#accessOrder.filter(n => n !== studentId);
        this.#cache.delete(studentId);
    }

    #updateAccess(studentId) {
        this.#accessOrder = this.#accessOrder.filter(n => n !== studentId);
        this.#accessOrder.push(studentId);
    }

    get size() {
        return this.#cache.size;
    }

    clear() {
        this.#cache.clear();
        this.#accessOrder = [];
    }

    // Get stats
    getStats() {
        return {
            size: this.#cache.size,
            maxSize: this.#maxSize,
            entries: [...this.#cache.entries()].map(([studentId, data]) => ({
                studentId,
                name: data.name
            }))
        };
    }
}

// Usage
const cache = new StudentCache(50);

// Cache student data
async function getStudent(studentId) {
    // Check the cache first
    const cached = cache.get(studentId);
    if (cached) {
        console.log('Cache hit:', studentId);
        return cached;
    }

    // Fetch from the server
    console.log('Cache miss:', studentId);
    const response = await fetch(`/api/students/${studentId}`);
    const student = await response.json();

    // Store in the cache
    cache.set(studentId, student);
    return student;
}

// Cache with TTL (Time To Live)
class TTLCache extends StudentCache {
    #ttl;

    constructor(maxSize, ttlMs = 60000) {
        super(maxSize);
        this.#ttl = ttlMs;
        this.#timestamps = new Map();
    }

    set(studentId, data) {
        super.set(studentId, data);
        this.#timestamps.set(studentId, Date.now());
    }

    get(studentId) {
        const timestamp = this.#timestamps.get(studentId);
        if (timestamp && Date.now() - timestamp > this.#ttl) {
            this.delete(studentId);
            this.#timestamps.delete(studentId);
            return null;
        }
        return super.get(studentId);
    }
}

// Usage
const ttlCache = new TTLCache(50, 5 * 60 * 1000); // 5 minutes

Example: Unique Course IDs with a Set

class EnrollmentTracker {
    #courses;
    #students;
    #enrollments;

    constructor() {
        this.#courses = new Map();
        this.#students = new Map();
        this.#enrollments = new Map();
    }

    addCourse(course) {
        this.#courses.set(course.id, course);
    }

    addStudent(student) {
        this.#students.set(student.studentId, student);
        // A Set to track enrollment per student
        this.#enrollments.set(student.studentId, new Set());
    }

    enroll(studentId, courseId) {
        if (!this.#students.has(studentId)) {
            throw new Error('Student not registered');
        }
        if (!this.#courses.has(courseId)) {
            throw new Error('Course not found');
        }

        const studentCourses = this.#enrollments.get(studentId);

        // Check for duplicates
        if (studentCourses.has(courseId)) {
            throw new Error('Already enrolled in this course');
        }

        studentCourses.add(courseId);
    }

    drop(studentId, courseId) {
        const studentCourses = this.#enrollments.get(studentId);
        if (studentCourses) {
            studentCourses.delete(courseId);
        }
    }

    getStudentCourses(studentId) {
        const courseIds = this.#enrollments.get(studentId) || new Set();
        return [...courseIds].map(id => this.#courses.get(id)).filter(Boolean);
    }

    getCourseStudents(courseId) {
        const result = [];
        for (const [studentId, courses] of this.#enrollments) {
            if (courses.has(courseId)) {
                result.push(this.#students.get(studentId));
            }
        }
        return result;
    }

    // Check whether two students share a course
    hasCommonCourse(studentId1, studentId2) {
        const courses1 = this.#enrollments.get(studentId1) || new Set();
        const courses2 = this.#enrollments.get(studentId2) || new Set();

        for (const course of courses1) {
            if (courses2.has(course)) return true;
        }
        return false;
    }

    // Get all unique course IDs
    getAllUniqueCourseIds() {
        const allIds = new Set();
        for (const courses of this.#enrollments.values()) {
            for (const id of courses) {
                allIds.add(id);
            }
        }
        return allIds;
    }

    // Enrollment statistics
    getEnrollmentStats() {
        const courseCounts = new Map();

        for (const courses of this.#enrollments.values()) {
            for (const courseId of courses) {
                courseCounts.set(
                    courseId,
                    (courseCounts.get(courseId) || 0) + 1
                );
            }
        }

        return [...courseCounts.entries()]
            .map(([id, count]) => ({
                course: this.#courses.get(id),
                studentCount: count
            }))
            .sort((a, b) => b.studentCount - a.studentCount);
    }
}

// Usage
const tracker = new EnrollmentTracker();

tracker.addCourse({ id: 'IF101', name: 'Algorithms', credits: 3 });
tracker.addCourse({ id: 'IF102', name: 'Web Programming', credits: 3 });
tracker.addCourse({ id: 'IF103', name: 'Database', credits: 3 });

tracker.addStudent({ studentId: '001', name: 'Mike' });
tracker.addStudent({ studentId: '002', name: 'John' });

tracker.enroll('001', 'IF101');
tracker.enroll('001', 'IF102');
tracker.enroll('002', 'IF102');
tracker.enroll('002', 'IF103');

console.log(tracker.getStudentCourses('001'));
// [{ id: 'IF101', ... }, { id: 'IF102', ... }]

console.log(tracker.hasCommonCourse('001', '002')); // true (IF102)

console.log(tracker.getEnrollmentStats());
// [{ course: { id: 'IF102', ... }, studentCount: 2 }, ...]

Example: Grade Book with a Map

class GradeBook {
    #grades;
    #students;

    constructor() {
        this.#grades = new Map(); // studentId -> Map(mkId -> grade)
        this.#students = new Map();
    }

    addStudent(studentId, name) {
        this.#students.set(studentId, name);
        this.#grades.set(studentId, new Map());
    }

    addGrade(studentId, mkId, grade) {
        const studentGrades = this.#grades.get(studentId);
        if (!studentGrades) throw new Error('Student not found');
        studentGrades.set(mkId, grade);
    }

    getStudentGrades(studentId) {
        const grades = this.#grades.get(studentId);
        if (!grades) return {};

        const result = {};
        for (const [mkId, grade] of grades) {
            result[mkId] = grade;
        }
        return result;
    }

    getAverage(studentId) {
        const grades = this.#grades.get(studentId);
        if (!grades || grades.size === 0) return 0;

        let total = 0;
        for (const grade of grades.values()) {
            total += grade;
        }
        return total / grades.size;
    }

    getCourseAverage(mkId) {
        let total = 0;
        let count = 0;

        for (const grades of this.#grades.values()) {
            if (grades.has(mkId)) {
                total += grades.get(mkId);
                count++;
            }
        }

        return count > 0 ? total / count : 0;
    }

    getTopStudents(n = 5) {
        return [...this.#grades.entries()]
            .map(([studentId, grades]) => {
                let total = 0;
                for (const grade of grades.values()) total += grade;
                return {
                    studentId,
                    name: this.#students.get(studentId),
                    average: grades.size > 0 ? total / grades.size : 0,
                    courseCount: grades.size
                };
            })
            .sort((a, b) => b.average - a.average)
            .slice(0, n);
    }

    getGradeDistribution(mkId) {
        const dist = { A: 0, B: 0, C: 0, D: 0, E: 0 };

        for (const grades of this.#grades.values()) {
            if (grades.has(mkId)) {
                const grade = grades.get(mkId);
                if (grade >= 85) dist.A++;
                else if (grade >= 75) dist.B++;
                else if (grade >= 65) dist.C++;
                else if (grade >= 50) dist.D++;
                else dist.E++;
            }
        }

        return dist;
    }
}

// Usage
const gradeBook = new GradeBook();
gradeBook.addStudent('001', 'Mike');
gradeBook.addStudent('002', 'John');
gradeBook.addStudent('003', 'Anna');

gradeBook.addGrade('001', 'IF101', 88);
gradeBook.addGrade('001', 'IF102', 92);
gradeBook.addGrade('002', 'IF101', 75);
gradeBook.addGrade('002', 'IF102', 85);
gradeBook.addGrade('003', 'IF101', 90);

console.log(gradeBook.getAverage('001')); // 90
console.log(gradeBook.getCourseAverage('IF101')); // 84.33
console.log(gradeBook.getTopStudents(2));
console.log(gradeBook.getGradeDistribution('IF101'));

Code Explanation

Map Methods

  • set(key, value) - Add/update an entry, returns the Map (chainable)
  • get(key) - Get the value, undefined if it doesn't exist
  • has(key) - Check for the existence of a key, returns a boolean
  • delete(key) - Remove an entry, returns a boolean
  • clear() - Remove all entries
  • size - Number of entries
  • entries() / keys() / values() - Iterator
  • forEach() - Iterate over all entries

Set Methods

  • add(value) - Add a value, returns the Set
  • has(value) - Check for the existence of a value
  • delete(value) - Remove a value
  • clear() - Remove all values
  • size - Number of values

WeakMap / WeakSet

  • Can only store objects as keys/values
  • Cannot be iterated
  • Does not have a size property
  • Data is automatically garbage collected when no other references remain

Conclusion

Map and Set are powerful data structures for collection manipulation in JavaScript. A Map is ideal for caching, lookup tables, and data that requires insertion order. A Set is ideal for tracking unique items, set theory operations (union, intersection, difference), and duplicate validation. For a campus application, use a Map for caching student data and a grade book, and a Set for tracking enrollment and ensuring there are no duplicates. WeakMap and WeakSet are useful for metadata that must be cleaned up automatically.