Skip to main content
JavaScript beginner Lesson 10 of 24

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

Frequently Asked Questions

What is the difference between Object.assign and the spread operator for merging objects?
Both perform a shallow merge. Object.assign mutates the first argument (the target) and returns it. The spread operator always creates a new object. Prefer spread for immutable patterns; use Object.assign when you intentionally want to mutate a target.
Does spreading an object create a deep clone?
No. Spread only copies the top-level properties. Nested objects are still shared by reference. For a deep clone use structuredClone() (modern browsers/Node 17+) or JSON.parse(JSON.stringify(obj)) for plain data.
What are computed property keys?
Computed property keys let you use an expression inside square brackets as the key name when defining an object literal, e.g. { [variableName]: value }. This is handy when the key is dynamic.