Map and Set: Efficient Collections in Modern JavaScript
In a library, different types of materials have different management methods: dictionaries arrange entries alphabetically, journals are archived by date, and membership card systems record each unique member number. Traditional JavaScript objects and arrays can handle these tasks, but ES6's Map and Set provide more professional and efficient solutions. Set is like a membership system that doesn't allow duplicates, while Map is like a smart dictionary that can use anything as an index. They not only provide clearer semantics but also have significant advantages in performance and functionality.
Set - Collection of Unique Values
Set is a collection that stores unique values, automatically removing duplicates. It's like an auto-deduplication container—no matter how many times you add the same value, only one will ultimately be retained.
Creating and Basic Operations
// Create empty Set
let emptySet = new Set();
// Create Set with initial values
let numbers = new Set([1, 2, 3, 4, 5]);
console.log(numbers); // Set(5) { 1, 2, 3, 4, 5 }
// Auto deduplication
let withDuplicates = new Set([1, 2, 2, 3, 3, 3, 4]);
console.log(withDuplicates); // Set(4) { 1, 2, 3, 4 }
// Add values
numbers.add(6);
console.log(numbers.size); // 6
// Adding duplicate values has no effect
numbers.add(3);
console.log(numbers.size); // 6 (still 6)
// Check if value exists
console.log(numbers.has(3)); // true
console.log(numbers.has(10)); // false
// Delete value
numbers.delete(3);
console.log(numbers.has(3)); // false
// Clear Set
numbers.clear();
console.log(numbers.size); // 0Set's core methods are simple and clear: add() adds values, has() checks existence, delete() deletes values, clear() empties the collection, and size property gets the element count.
Iterating Over Set
Set is iterable and can be traversed in multiple ways:
let colors = new Set(["red", "green", "blue", "yellow"]);
// Using for...of
for (let color of colors) {
console.log(color);
}
// Using forEach
colors.forEach((color) => {
console.log(color);
});
// Set's forEach callback receives three parameters: value, value (yes, second parameter is also value), Set itself
colors.forEach((value, valueAgain, set) => {
console.log(`${value} === ${valueAgain}`); // true
});
// Convert to array
let colorArray = [...colors];
console.log(colorArray); // ["red", "green", "blue", "yellow"]
// Using Array.from
let colorArray2 = Array.from(colors);Practical Application: Array Deduplication
One of the most common uses of Set is array deduplication:
let products = [
"laptop",
"mouse",
"keyboard",
"mouse",
"laptop",
"monitor",
"keyboard",
"headphones",
];
// Use Set for deduplication
let uniqueProducts = [...new Set(products)];
console.log(uniqueProducts);
// ["laptop", "mouse", "keyboard", "monitor", "headphones"]
// Count unique values
function countUnique(arr) {
return new Set(arr).size;
}
console.log(countUnique(products)); // 5Set Operations
Set can conveniently implement mathematical set operations:
let setA = new Set([1, 2, 3, 4]);
let setB = new Set([3, 4, 5, 6]);
// Union
let union = new Set([...setA, ...setB]);
console.log([...union]); // [1, 2, 3, 4, 5, 6]
// Intersection
let intersection = new Set([...setA].filter((x) => setB.has(x)));
console.log([...intersection]); // [3, 4]
// Difference - elements in A but not in B
let difference = new Set([...setA].filter((x) => !setB.has(x)));
console.log([...difference]); // [1, 2]
// Symmetric Difference - elements in A or B but not in both
let symmetricDiff = new Set([
[...setA].filter((x) => !setB.has(x)),
[...setB].filter((x) => !setA.has(x)),
]);
console.log([...symmetricDiff]); // [1, 2, 5, 6]Practical Application: Tag System
class TagManager {
constructor() {
this.tags = new Set();
}
// Add single tag
addTag(tag) {
this.tags.add(tag.toLowerCase());
}
// Batch add tags
addTags(...tags) {
tags.forEach(tag => this.tags.add(tag.toLowerCase()));
}
// Delete tag
removeTag(tag) {
this.tags.delete(tag.toLowerCase());
}
// Check if tag exists
hasTag(tag) {
return this.tags.has(tag.toLowerCase());
}
// Get all tags
getAllTags() {
return Array.from(this.tags).sort();
}
// Clear tags
clear() {
this.tags.clear();
}
// Tag count
count() {
return this.tags.size;
}
}
// Usage example
let articleTags = new TagManager();
articleTags.addTags("javascript", "programming", "web");
articleTags.addTag("JAVASCRIPT"); // Auto deduplication (lowercase makes them same)
console.log(articleTags.getAllTags());
// ["javascript", "programming", "web"]
console.log(articleTags.hasTag("Web")); // true (case insensitive)Map - Key-Value Collection
Map is a key-value pair collection, similar to objects but with several important differences: keys can be any type (not just strings), insertion order is maintained, and it provides richer methods.
Creating and Basic Operations
// Create empty Map
let emptyMap = new Map();
// Create Map from array
let userRoles = new Map([
["alice", "admin"],
["bob", "editor"],
["charlie", "viewer"],
]);
// Set key-value pairs
let config = new Map();
config.set("theme", "dark");
config.set("language", "en");
config.set("fontSize", 14);
// Chaining calls
let settings = new Map()
.set("host", "localhost")
.set("port", 3000)
.set("timeout", 5000);
// Get value
console.log(config.get("theme")); // "dark"
console.log(config.get("missing")); // undefined
// Check if key exists
console.log(config.has("theme")); // true
console.log(config.has("color")); // false
// Delete key-value pair
config.delete("fontSize");
console.log(config.has("fontSize")); // false
// Get size
console.log(userRoles.size); // 3
// Clear Map
config.clear();
console.log(config.size); // 0Any Type Can Be Used as Key
This is Map's biggest advantage over regular objects:
let map = new Map();
// Object as key
let objKey = { id: 1 };
map.set(objKey, "Object as key");
console.log(map.get(objKey)); // "Object as key"
// Function as key
let funcKey = function () {};
map.set(funcKey, "Function as key");
// Array as key
let arrKey = [1, 2, 3];
map.set(arrKey, "Array as key");
// DOM element as key (in browser)
// let buttonKey = document.querySelector('button');
// map.set(buttonKey, 'Button element');
// Map as key
let mapKey = new Map();
map.set(mapKey, "Map as key");
console.log(map.size); // 4
// Note: same reference is required to find value
console.log(map.get({ id: 1 })); // undefined (different object references)
console.log(map.get(objKey)); // "Object as key" (same reference)Iterating Over Map
let prices = new Map([
["laptop", 1299],
["mouse", 29],
["keyboard", 89],
["monitor", 399],
]);
// Use for...of to iterate key-value pairs
for (let [product, price] of prices) {
console.log(`${product}: $${price}`);
}
// Use forEach
prices.forEach((price, product) => {
console.log(`${product}: $${price}`);
});
// Only iterate keys
for (let product of prices.keys()) {
console.log(product);
}
// Only iterate values
for (let price of prices.values()) {
console.log(price);
}
// Get all entries
console.log([...prices.entries()]);
// [["laptop", 1299], ["mouse", 29], ...]Map vs Object
Let's compare Map and regular objects:
// 1. Key types
let obj = {};
let map = new Map();
// Object: keys are converted to strings
obj[1] = "one";
obj[true] = "yes";
console.log(Object.keys(obj)); // ["1", "true"] - all become strings
// Map: keys maintain original type
map.set(1, "one");
map.set(true, "yes");
console.log([...map.keys()]); // [1, true] - maintain original type
// 2. Performance
// Map performs better when frequently adding/deleting key-value pairs
console.time("Object");
let testObj = {};
for (let i = 0; i < 10000; i++) {
testObj[`key${i}`] = i;
}
for (let i = 0; i < 10000; i++) {
delete testObj[`key${i}`];
}
console.timeEnd("Object");
console.time("Map");
let testMap = new Map();
for (let i = 0; i < 10000; i++) {
testMap.set(`key${i}`, i);
}
for (let i = 0; i < 10000; i++) {
testMap.delete(`key${i}`);
}
console.timeEnd("Map");
// 3. Order guarantee
// Map guarantees traversal order is insertion order
let orderedMap = new Map();
orderedMap.set("z", 1);
orderedMap.set("a", 2);
orderedMap.set("m", 3);
console.log([...orderedMap.keys()]); // ["z", "a", "m"] - insertion order
// 4. Size access
console.log(map.size); // Direct property
console.log(Object.keys(obj).length); // Needs calculationPractical Application: Caching System
class Cache {
constructor(maxSize = 100) {
this.cache = new Map();
this.maxSize = maxSize;
}
// Get cached value
get(key) {
if (!this.cache.has(key)) {
return null;
}
// LRU: move accessed item to end (most recently used)
let value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
// Set cached value
set(key, value) {
// If key already exists, delete it first
if (this.cache.has(key)) {
this.cache.delete(key);
}
// If exceeding max capacity, delete oldest item (first one)
if (this.cache.size >= this.maxSize) {
let firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
// Check if has cache
has(key) {
return this.cache.has(key);
}
// Clear cache
clear() {
this.cache.clear();
}
// Get cache size
size() {
return this.cache.size;
}
// Get all keys
keys() {
return Array.from(this.cache.keys());
}
}
// Usage example
let apiCache = new Cache(3); // Cache at most 3 items
apiCache.set("/api/users", [{ id: 1, name: "Alice" }]);
apiCache.set("/api/products", [{ id: 101, name: "Laptop" }]);
apiCache.set("/api/orders", [{ id: 1001, status: "pending" }]);
console.log(apiCache.keys());
// ["/api/users", "/api/products", "/api/orders"]
// Adding fourth item will remove the oldest
apiCache.set("/api/settings", { theme: "dark" });
console.log(apiCache.keys());
// ["/api/products", "/api/orders", "/api/settings"]
// "/api/users" was removed
// Accessing an item moves it to the end
apiCache.get("/api/products");
console.log(apiCache.keys());
// ["/api/orders", "/api/settings", "/api/products"]
// "/api/products" was moved to the endObject and Map Conversion
// Object to Map
let obj = {
name: "Emma",
age: 24,
city: "London",
};
let mapFromObj = new Map(Object.entries(obj));
console.log(mapFromObj.get("name")); // "Emma"
// Map to Object
let map = new Map([
["theme", "dark"],
["language", "en"],
["notifications", true],
]);
let objFromMap = Object.fromEntries(map);
console.log(objFromMap);
// { theme: "dark", language: "en", notifications: true }Real-world Case: User Permission Management System
Let's comprehensively use Map and Set to build a permission management system:
class PermissionSystem {
constructor() {
// Use Map to store user permissions (user ID -> Set<permissions>)
this.userPermissions = new Map();
// Use Map to store role permissions (role name -> Set<permissions>)
this.rolePermissions = new Map();
// Use Map to store user roles (user ID -> Set<roles>)
this.userRoles = new Map();
}
// Define role permissions
defineRole(roleName, permissions) {
this.rolePermissions.set(roleName, new Set(permissions));
}
// Assign roles to users
assignRole(userId, roleName) {
if (!this.userRoles.has(userId)) {
this.userRoles.set(userId, new Set());
}
this.userRoles.get(userId).add(roleName);
}
// Grant direct permissions to users
grantPermission(userId, permission) {
if (!this.userPermissions.has(userId)) {
this.userPermissions.set(userId, new Set());
}
this.userPermissions.get(userId).add(permission);
}
// Revoke direct user permissions
revokePermission(userId, permission) {
if (this.userPermissions.has(userId)) {
this.userPermissions.get(userId).delete(permission);
}
}
// Get all user permissions (role permissions + direct permissions)
getUserPermissions(userId) {
let permissions = new Set();
// Add direct permissions
if (this.userPermissions.has(userId)) {
for (let perm of this.userPermissions.get(userId)) {
permissions.add(perm);
}
}
// Add role permissions
if (this.userRoles.has(userId)) {
for (let role of this.userRoles.get(userId)) {
if (this.rolePermissions.has(role)) {
for (let perm of this.rolePermissions.get(role)) {
permissions.add(perm);
}
}
}
}
return permissions;
}
// Check if user has specific permission
hasPermission(userId, permission) {
let permissions = this.getUserPermissions(userId);
return permissions.has(permission);
}
// Check if user has all specified permissions
hasAllPermissions(userId, ...requiredPermissions) {
let permissions = this.getUserPermissions(userId);
return requiredPermissions.every((perm) => permissions.has(perm));
}
// Check if user has any of specified permissions
hasAnyPermission(userId, ...requiredPermissions) {
let permissions = this.getUserPermissions(userId);
return requiredPermissions.some((perm) => permissions.has(perm));
}
// Get statistics
getStats() {
return {
totalUsers: this.userRoles.size,
totalRoles: this.rolePermissions.size,
rolesInfo: Array.from(this.rolePermissions.entries()).map(
([role, perms]) => ({
role,
permissionCount: perms.size,
permissions: Array.from(perms),
})
),
};
}
}
// Usage example
let permSystem = new PermissionSystem();
// Define roles
permSystem.defineRole("admin", [
"read",
"write",
"delete",
"manage_users",
"manage_settings",
]);
permSystem.defineRole("editor", ["read", "write", "edit"]);
permSystem.defineRole("viewer", ["read"]);
// Assign roles to users
permSystem.assignRole("user_001", "admin");
permSystem.assignRole("user_002", "editor");
permSystem.assignRole("user_003", "viewer");
// user_002 gets additional direct permission
permSystem.grantPermission("user_002", "manage_comments");
// Check permissions
console.log(permSystem.hasPermission("user_001", "delete")); // true
console.log(permSystem.hasPermission("user_002", "delete")); // false
console.log(permSystem.hasPermission("user_002", "manage_comments")); // true
// Get all user permissions
console.log([...permSystem.getUserPermissions("user_002")]);
// ["read", "write", "edit", "manage_comments"]
// Check multiple permissions
console.log(
permSystem.hasAllPermissions("user_001", "read", "write", "delete")
);
// true
console.log(permSystem.hasAnyPermission("user_003", "write", "delete"));
// false (viewer only has read permission)
// Get statistics
console.log(permSystem.getStats());
// {
// totalUsers: 3,
// totalRoles: 3,
// rolesInfo: [...]
// }Performance Comparison
Understanding the performance differences between Map/Set and traditional approaches is important:
let size = 10000;
// Set vs Array (deduplication)
console.time("Array unique");
let arr = [];
for (let i = 0; i < size; i++) {
if (!arr.includes(i % 100)) {
arr.push(i % 100);
}
}
console.timeEnd("Array unique");
console.time("Set unique");
let set = new Set();
for (let i = 0; i < size; i++) {
set.add(i % 100);
}
console.timeEnd("Set unique");
// Set is much faster!
// Map vs Object (lookup)
console.time("Object lookup");
let obj = {};
for (let i = 0; i < size; i++) {
obj[`key${i}`] = i;
}
for (let i = 0; i < size; i++) {
let value = obj[`key${i}`];
}
console.timeEnd("Object lookup");
console.time("Map lookup");
let map = new Map();
for (let i = 0; i < size; i++) {
map.set(`key${i}`, i);
}
for (let i = 0; i < size; i++) {
let value = map.get(`key${i}`);
}
console.timeEnd("Map lookup");
// Performance is similar, but Map has more featuresSelection Guide
Use Set when:
- Need to store unique values
- Need to quickly check if a value exists
- Need deduplication
- Need set operations (union, intersection, etc.)
Use Map when:
- Need key-value storage, and keys aren't just strings
- Need frequent adding/deleting of key-value pairs
- Need to maintain insertion order
- Need high-performance key lookup
Use Object when:
- Need JSON serialization (Map/Set can't be directly JSON.stringify'd)
- Keys are definitely strings or symbols
- Need to use prototype inheritance
- Data structure is relatively fixed
Use Array when:
- Need indexed access
- Need array-specific methods (map, filter, reduce, etc.)
- Allow duplicate values
- Need sorting
Common Pitfalls
1. Set's Equality Judgment
let set = new Set();
set.add({ id: 1 });
set.add({ id: 1 }); // Different object references, won't deduplicate
console.log(set.size); // 2 (two objects)
// Basic types deduplicate normally
let nums = new Set();
nums.add(1);
nums.add(1);
console.log(nums.size); // 12. Map Key Comparison
let map = new Map();
// NaN as key
map.set(NaN, "value");
console.log(map.get(NaN)); // "value" (Map thinks NaN === NaN)
// +0 and -0 are considered the same
map.set(+0, "plus zero");
map.set(-0, "minus zero");
console.log(map.size); // 1 (two keys considered the same)3. Conversion Limitations
let map = new Map();
map.set({ id: 1 }, "object key");
// JSON.stringify can't directly serialize Map
console.log(JSON.stringify(map)); // "{}" - data lost!
// Correct way: convert to array first
let serialized = JSON.stringify([...map]);
let deserialized = new Map(JSON.parse(serialized));Summary
Map and Set are powerful data structures introduced in ES6:
- Set - Collection of unique values, automatic deduplication, efficient existence checking
- Map - Collection of key-value pairs, any type of keys, maintains insertion order, excellent performance
Compared to traditional objects and arrays, they provide:
- Clearer semantics (clear intent)
- Better performance (especially with large data volumes)
- Richer functionality (like size property, dedicated iteration methods)
- More flexible key types (Map)
In modern JavaScript development, reasonably choosing and using Map, Set, Object, Array can significantly improve code quality and performance. Use Set when you need uniqueness, use Map when you need flexible key-value pairs—they will make your code more concise and efficient.