JavaScript gives you three core collection types. Plain objects are flexible key-value stores with prototype-based methods. Map is a dedicated hash-table that accepts any key type. Set holds unique values. This lesson walks through each and the patterns where one beats the others.
Object literals
The object literal is the most common way to create an object. Keys are strings or symbols. Values can be any type. Use the shorthand syntax when the key name matches a variable name.
const name = "Asha";
const role = "admin";
const user = { name, role, age: 28 };
console.log(user); // { name: 'Asha', role: 'admin', age: 28 }
Access, mutation, and optional chaining
Use dot or bracket notation to read and write. Bracket notation is required when the key is dynamic. Optional chaining ?. returns the missing-value primitive if the path is broken instead of throwing.
const user = { profile: { settings: { theme: "dark" } } };
console.log(user.profile.settings.theme); // dark
console.log(user["profile"]["settings"]["theme"]); // dark
const key = "role";
console.log(user[key]); // admin
console.log(user?.profile?.billing?.plan); // returns the missing-value primitive (no throw)
Object destructuring
Pull multiple properties into local names in one line. Rename with the colon syntax. Default values fill in when a property is missing. Rest collects the leftover properties into a new object.
const user = { name: "Asha", role: "admin", age: 28 };
const { name, age, role = "viewer" } = user;
console.log(name, age, role); // Asha 28 admin
const { name: userName, ...rest } = user;
console.log(userName, rest); // Asha { role: 'admin', age: 28 }
Useful Object methods
Object.keys gives an array of keys, Object.values gives values, Object.entries gives key-value pairs. Object.fromEntries builds an object from those pairs. Object.assign and the spread operator do shallow copies and merges.
const user = { name: "Asha", role: "admin", age: 28 };
console.log(Object.keys(user)); // ['name', 'role', 'age']
console.log(Object.values(user)); // ['Asha', 'admin', 28]
console.log(Object.entries(user)); // [['name', 'Asha'], ['role', 'admin'], ['age', 28]]
const extra = { city: "Pune" };
const merged = { ...user, ...extra };
console.log(merged); // { name: 'Asha', role: 'admin', age: 28, city: 'Pune' }
Map for any-key collections
Map accepts any value as a key, including objects. It preserves insertion order and has a clean size property. Use Map when you need to use non-string keys, when you add and remove frequently, or when you want predictable iteration order.
const cache = new Map();
const key = { id: 1 };
cache.set(key, "value for object key");
console.log(cache.get(key)); // value for object key
console.log(cache.size); // 1
// Map from entries
const map = new Map([["a", 1], ["b", 2]]);
for (const [k, v] of map) {
console.log(k, v);
}
Set for unique collections
Set stores unique values of any type. It shines for deduplication and O(1) membership tests. Iteration order is insertion order. Use Array.from(set) to get an array back.
const unique = new Set([1, 2, 2, 3, 3, 3, 4]);
console.log([...unique]); // [1, 2, 3, 4]
const tags = new Set();
tags.add("javascript");
tags.add("python");
tags.add("javascript");
console.log(tags.size); // 2
console.log(tags.has("js")); // false (case sensitive)
Summary
Use plain objects for simple string-keyed records. Use Map when keys can be any type, when you need a clean size property, or when you frequently add and remove entries. Use Set for unique collections and fast membership tests. The next lesson covers functions, scope, and closures, the heart of how JavaScript code is organized.
When to use a plain object versus a Map
Pick a plain object when the shape is fixed and the keys are strings known at write time. Pick a Map when keys are user input, when keys are objects, when the size matters, or when you add and remove entries frequently. The Map API is explicit about set, get, has, and delete, which makes the code read like a dictionary in another language. A plain object inherits from Object.prototype, so keys like toString or constructor collide with built-in methods; Map does not have that problem.
// Watch out for inherited keys on plain objects
const counts = { toString: 1 };
console.log(counts.toString); // 1 -- shadows Object.prototype.toString
console.log("toString" in counts); // true
const safe = new Map();
safe.set("toString", 1);
console.log(safe.get("toString")); // 1
console.log(safe.has("toString")); // true
console.log(safe.size); // 1
Shallow versus deep copies
The spread operator and Object.assign copy one level deep. If a property holds an object or array, both the original and the copy share that nested reference. Use structuredClone for a true deep copy in modern JavaScript, or write a custom copy for the shapes you control.
const original = { name: "Asha", tags: ["js", "ts"] };
const shallow = { ...original };
shallow.tags.push("go");
console.log(original.tags); // ["js", "ts", "go"] -- shared!
const deep = structuredClone(original);
deep.tags.push("rust");
console.log(original.tags); // ["js", "ts", "go"] -- not shared
console.log(deep.tags); // ["js", "ts", "go", "rust"]
Common pitfalls
The most common bug with object property access is forgetting that JavaScript silently returns the missing-value primitive for missing keys instead of raising an error. The second most common is mutating an object you did not intend to mutate, because the variable holds a reference, not a copy. Treat objects as references unless you have spread, slice, or structuredClone to break the link. The third is treating a plain object as if it were ordered: since ES2015 the language guarantees insertion order for string keys, but integer-like keys always sort numerically first. Use an array or a Map when the order must be respected.