Objects in JavaScript
Learn JavaScript objects — literals, destructuring, spread/rest, Object utility methods, symbols, and real-world patterns like config merging and deep clone warnings.
Objects are the universal data structure in JavaScript — almost everything, from DOM nodes to API responses, is an object. A plain object is a collection of key-value pairs where the keys are strings (or Symbols) and the values can be anything. Knowing modern object syntax will make your code shorter, clearer, and more predictable.
Object Literals
Basic creation
Object literals are the simplest and most common way to create an object. Properties are accessed with dot notation for known key names and bracket notation when the key is dynamic or contains special characters.
const user = {
id: 1,
name: "Alice",
age: 30,
active: true,
};
// Dot notation for known keys
console.log(user.name); // "Alice"
// Bracket notation for dynamic or special keys
console.log(user["age"]); // 30
// Add or update a property at any time
user.email = "[email protected]";
user.age = 31;
// Remove a property
delete user.active;
Property shorthand
When you’re building an object from existing variables and the variable names match the desired key names, you can omit the value entirely. This shorthand is pervasive in modern JavaScript — you’ll see it constantly in function returns, destructured imports, and React components.
const name = "Bob";
const age = 25;
const admin = false;
// Old way — redundant repetition
const userOld = { name: name, age: age, admin: admin };
// Shorthand — cleaner and less error-prone
const userNew = { name, age, admin };
Method shorthand
The method shorthand removes the function keyword from method definitions inside object literals, making object APIs look cleaner. Methods defined this way have their own this bound correctly to the object, unlike arrow functions.
const calculator = {
value: 0,
// Old: add: function(n) { ... }
add(n) { this.value += n; return this; }, // return this for chaining
subtract(n) { this.value -= n; return this; },
result() { return this.value; },
};
console.log(calculator.add(10).add(5).subtract(3).result()); // 12
Computed property keys
Computed property keys let you use any expression as a key name by wrapping it in square brackets inside the object literal. This is the right tool when key names are dynamic — when they come from a variable, a template literal, or are computed at runtime.
const field = "email";
const index = 2;
const record = {
[field]: "[email protected]", // key is the value of `field`
[`item_${index}`]: "banana", // key is a computed template literal
};
console.log(record.email); // "[email protected]"
console.log(record.item_2); // "banana"
// Useful for building objects dynamically
function createValidator(rules) {
return rules.reduce((obj, rule) => {
obj[rule.name] = rule.fn;
return obj;
}, {});
}
Destructuring
Destructuring lets you unpack properties from an object into named variables in a single statement. It eliminates repetitive const x = obj.x patterns and makes function signatures self-documenting. You can rename variables, provide defaults, and nest destructuring as deeply as needed.
Basic destructuring
const product = { id: 42, name: "Laptop", price: 999, inStock: true };
// Extract multiple properties in one statement
const { id, name, price } = product;
console.log(id, name, price); // 42 "Laptop" 999
// The original object is unchanged
Renaming during destructuring
// Use a colon to rename: { original: alias }
const { name: productName, price: cost } = product;
console.log(productName, cost); // "Laptop" 999
Default values
// Defaults apply when the property is undefined (missing or explicitly undefined)
const { name: label, discount = 0, category = "general" } = product;
console.log(discount); // 0 — product has no discount property
console.log(category); // "general" — product has no category property
Nested destructuring
const order = {
id: "ORD-001",
customer: { name: "Alice", city: "London" },
total: 150,
};
// Destructure nested objects in one expression
const { id: orderId, customer: { name: customerName, city } } = order;
console.log(orderId, customerName, city); // "ORD-001" "Alice" "London"
Destructuring in function parameters
Destructuring function parameters is one of the most practical applications. It documents what the function expects, provides defaults for optional fields, and avoids cluttering the function body with const x = options.x boilerplate.
function formatAddress({ street, city, country = "US", zip }) {
return `${street}, ${city}, ${country} ${zip}`;
}
formatAddress({ street: "123 Main St", city: "Austin", zip: "73301" });
// "123 Main St, Austin, US 73301"
Spread and Rest
Spread — expand an object into another
The spread operator copies all enumerable own properties of an object into a new one. When keys overlap, later entries win — which makes it the cleanest way to merge objects or override specific properties without mutating the original.
const defaults = { theme: "light", fontSize: 14, language: "en" };
const userPrefs = { fontSize: 16, language: "fr" };
// Merge: later keys override earlier ones
const config = { ...defaults, ...userPrefs };
// { theme: "light", fontSize: 16, language: "fr" }
// Override a single field immutably
const updated = { ...config, theme: "dark" };
Rest — collect remaining properties
The rest syntax in destructuring collects all properties that weren’t explicitly named into a new object. It’s the clean way to separate a known subset of properties from the rest — useful for forwarding props, removing keys, or splitting a config object.
const { id, name, ...rest } = { id: 1, name: "Alice", age: 30, city: "London" };
console.log(id); // 1
console.log(name); // "Alice"
console.log(rest); // { age: 30, city: "London" }
// Useful for omitting keys without mutating
function omit(obj, ...keys) {
const keySet = new Set(keys);
return Object.fromEntries(
Object.entries(obj).filter(([k]) => !keySet.has(k))
);
}
omit({ a: 1, b: 2, c: 3 }, "b"); // { a: 1, c: 3 }
Object Utility Methods
Object.keys, Object.values, Object.entries
These three methods convert an object’s structure into arrays, making it compatible with all the array iteration methods like map, filter, and reduce. Object.fromEntries is the inverse — it turns an array of [key, value] pairs back into an object.
const scores = { alice: 90, bob: 78, carol: 95 };
Object.keys(scores); // ["alice", "bob", "carol"]
Object.values(scores); // [90, 78, 95]
Object.entries(scores); // [["alice", 90], ["bob", 78], ["carol", 95]]
// Sum all values using array methods
const total = Object.values(scores).reduce((s, n) => s + n, 0); // 263
// Transform all values and rebuild as an object
const curved = Object.fromEntries(
Object.entries(scores).map(([name, score]) => [name, Math.min(score + 5, 100)])
);
// { alice: 95, bob: 83, carol: 100 }
Object.assign
Object.assign copies properties from one or more source objects into a target object, mutating the target. It’s most commonly used to merge without mutation by passing an empty object as the target — though the spread operator is generally preferred for that pattern today.
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
Object.assign(target, source); // mutates target — b is overwritten
console.log(target); // { a: 1, b: 3, c: 4 }
// Merge without mutation: use an empty object as the target
const merged = Object.assign({}, target, source);
Object.freeze and Object.seal
Object.freeze prevents all modifications to an object — no new properties, no deletions, no value changes. It’s useful for constants and configuration objects that must never change. The important caveat is that freeze is shallow: nested objects are not frozen and can still be mutated.
const CONFIG = Object.freeze({
API_URL: "https://api.example.com",
TIMEOUT: 5000,
MAX_RETRIES: 3,
});
CONFIG.TIMEOUT = 9999; // silently ignored in sloppy mode, throws in strict mode
CONFIG.NEW_KEY = "value"; // same — no effect
// freeze is SHALLOW — nested objects are still mutable
const obj = Object.freeze({ nested: { x: 1 } });
obj.nested.x = 99; // this works! nested object is not frozen
Symbols as Keys
Symbols create unique, non-enumerable keys — useful for metadata that shouldn’t interfere with normal object iteration. Because symbols don’t appear in Object.keys, for...in, or JSON.stringify, they’re ideal for attaching internal framework metadata to objects that external code will also use.
const ID = Symbol("id");
const CREATED_AT = Symbol("createdAt");
const record = {
name: "Alice",
[ID]: 1001, // symbol key — hidden from enumeration
[CREATED_AT]: new Date(),
};
console.log(record[ID]); // 1001
console.log(Object.keys(record)); // ["name"] — symbols are not enumerated
// Useful for library/framework metadata
const VALIDATOR = Symbol("validator");
function attachValidator(obj, fn) {
obj[VALIDATOR] = fn; // won't clash with any string-keyed property
return obj;
}
Real-World Patterns
Config object with defaults
A common pattern is to accept an options object with defaults baked in. Spreading the user’s options after the defaults means any provided key overrides the default, and unspecified keys fall back automatically.
function createHttpClient(options = {}) {
const config = {
baseURL: "https://api.example.com",
timeout: 5000,
headers: { "Content-Type": "application/json" },
retries: 3,
...options, // user overrides for top-level keys
headers: { // deep-merge headers specifically so Content-Type is preserved
"Content-Type": "application/json",
...options.headers,
},
};
return config;
}
const client = createHttpClient({ timeout: 10000, headers: { Authorization: "Bearer token" } });
// { baseURL: "...", timeout: 10000, headers: { Content-Type: "...", Authorization: "..." }, retries: 3 }
Deep clone warning
Spreading or Object.assign only copies top-level properties. For nested objects you need a proper deep clone, and there are important trade-offs between the available options.
// JSON round-trip — works for plain data, but has limitations:
// - Loses Date objects (becomes string)
// - Drops undefined, functions, and Symbols
// - Throws on circular references
const deepCopy = JSON.parse(JSON.stringify(original));
// Modern, correct: structuredClone (Node 17+, all modern browsers)
const deepCopy2 = structuredClone(original);
// Handles Date, Map, Set, ArrayBuffer — but NOT functions or Symbols
Building a lookup map from an array
Transforming an array into an object keyed by ID is a very common performance optimization. Instead of calling array.find(u => u.id === x) repeatedly (O(n) each time), you build the map once and look up by key in O(1).
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Carol" },
];
// Build once
const userById = Object.fromEntries(users.map(u => [u.id, u]));
// { 1: { id: 1, name: "Alice" }, 2: ..., 3: ... }
// Look up in O(1) instead of O(n)
console.log(userById[2].name); // "Bob"
Common Pitfalls
Checking if a key exists — use in or hasOwn, not truthiness:
const config = { debug: false, port: 0 };
// Wrong — both false and 0 are falsy, so these never run even when the key exists
if (config.debug) { }
if (config.port) { }
// Correct — checks for key presence, regardless of the value
if ("debug" in config) { } // true
if (Object.hasOwn(config, "port")) { } // true (modern, preferred over hasOwnProperty)
Object shorthand with methods vs arrow functions:
const obj = {
value: 42,
// Arrow: 'this' is the outer scope — likely undefined or window, not obj
getArrow: () => this.value,
// Method shorthand: 'this' is bound to obj at call time — correct
getMethod() { return this.value; },
};
Spreading copies references for nested objects:
const a = { x: { y: 1 } };
const b = { ...a }; // shallow copy — b.x and a.x point to the same object
b.x.y = 99;
console.log(a.x.y); // 99 — a was mutated through the shared reference