JavaScript Objects and Destructuring
AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.
Objects are how JavaScript groups related data. This chapter covers the basics, destructuring, and the reference semantics behind “I changed one object and another one changed too”.
Literals and property access
const user = {
name: "ann",
age: 30,
active: true,
};
Two ways to read a property:
user.name; // dot: keys that are valid identifiers
user["name"]; // brackets: dynamic keys, spaces, hyphens
const key = "age";
user[key]; // 30
user["date-of-birth"]; // dot syntax cannot express this
A missing property returns undefined rather than throwing:
user.city; // undefined
user.city.name; // TypeError: Cannot read properties of undefined
That second line is why optional chaining exists: user.city?.name returns undefined instead of throwing.
Shorthand and computed keys
const name = "ann";
const age = 30;
const user = { name, age }; // same as { name: name, age: age }
const field = "role";
const withRole = { name, [field]: "admin" };
// { name: "ann", role: "admin" }
Methods have a shorthand too:
const counter = {
count: 0,
inc() { // same as inc: function () {}
this.count += 1;
},
};
Method shorthand is a regular function, so this is the calling object. An arrow function here would inherit this from the surrounding scope and the method would not work.
Add, update, delete
const user = { name: "ann" };
user.age = 30; // add
user.name = "anna"; // update
delete user.age; // remove
Object.hasOwn(user, "age"); // false
Object.hasOwn(obj, key) checks own properties only. Use key in obj when inherited properties should count — rarely what you want for plain data.
Iterating an object
const user = { name: "ann", age: 30, active: true };
Object.keys(user); // ["name", "age", "active"]
Object.values(user); // ["ann", 30, true]
Object.entries(user); // [["name", "ann"], ["age", 30], ["active", true]]
Converting to an array and using array methods is the most flexible approach:
Object.entries(user)
.filter(([, value]) => typeof value === "string")
.map(([key, value]) => `${key}=${value}`);
// ["name=ann"]
Ordering follows the specification: string keys keep insertion order, while integer-like keys ("2", "10") come first in ascending numeric order. Do not rely on object key order for user-visible sorting; sort the entries explicitly.
Destructuring
const user = { name: "ann", age: 30, city: "Shenzhen" };
const { name, age } = user;
console.log(name, age); // "ann" 30
Renaming and defaults:
const { name: userName, role = "user" } = user;
console.log(userName); // "ann"
console.log(role); // "user"
Nesting:
const order = { id: 1, buyer: { name: "bob" } };
const { buyer: { name } } = order; // "bob"
Nested destructuring throws if the intermediate value is missing. Default the intermediate to an empty object:
const { buyer: { name } = {} } = order;
Arrays destructure by position, which is handy for swapping and skipping:
const [first, second] = [1, 2, 3];
const [, , third] = [1, 2, 3]; // 3
[first, second] = [second, first]; // swap
Destructuring in function parameters is covered in the functions chapter; it is the standard way to accept an options object.
Spread and shallow copies
const base = { name: "ann", role: "user" };
const admin = { ...base, role: "admin" };
console.log(admin); // { name: "ann", role: "admin" }
console.log(base); // { name: "ann", role: "user" }
Later properties win. This “new object from an old one” pattern is how immutable updates work in React-style code.
Spread is shallow. Nested objects are still shared:
const user = { name: "ann", address: { city: "Shenzhen" } };
const copy = { ...user };
copy.address.city = "Beijing";
console.log(user.address.city); // "Beijing"
To copy one level deeper without the reference sharing, spread at each level you intend to modify:
const updated = { ...user, address: { ...user.address, city: "Beijing" } };
For a full independent copy, use structuredClone() (Node 17+, all modern browsers):
const deepCopy = structuredClone(user);
deepCopy.address.city = "Beijing";
console.log(user.address.city); // "Shenzhen"
structuredClone handles dates, maps, sets, and cyclic references, but not functions or class prototypes.
Reference semantics
const a = { count: 1 };
const b = a;
b.count = 2;
console.log(a.count); // 2
b = a copies the reference; both names point at one object. Comparison behaves the same way:
{ count: 1 } === { count: 1 } // false, two objects
a === b // true, same object
For structural comparison, compare fields, or serialize both sides when key order is stable. JSON.stringify comparison is fragile; for real needs use a deep-equal helper.
Freezing
const config = Object.freeze({ retries: 3 });
config.retries = 5; // TypeError in strict mode, silent no-op otherwise
console.log(config.retries); // 3
Object.freeze is shallow, so nested objects stay mutable. It also does not prevent { ...config, retries: 5 }, which is exactly how immutable updates are supposed to work.
Common mistakes
1. Assuming assignment copies
const b = a aliases the object. Copy with spread, or structuredClone for nested data.
2. Treating spread as a deep copy
Nested objects are shared; mutating them leaks across copies.
3. Destructuring a missing nested object
const { a: { b } } = obj throws when a is undefined. Provide = {}.
4. for...in without filtering
Inherited enumerable properties show up. Use Object.keys() or Object.entries().
5. Comparing objects with ===
Always false unless it is the same reference.
6. Using an object as a map with arbitrary string keys
obj["__proto__"] and obj["constructor"] collide with the prototype. Use Map or Object.create(null) for untrusted keys.
Summary
- Dot access for fixed keys, brackets for dynamic ones.
- Iterate with
Object.keys()/values()/entries()and then use array methods. - Destructuring supports renaming, defaults, and nesting.
- Spread copies one level;
structuredClone()copies deeply. - Assignment and
===work on references, not contents.
Next: strings, template literals, and text handling.