JavaScript Objects

7 minute read

An object is an unordered collection of key/value pairs. Keys (also called properties) are strings or Symbols; values can be anything. Objects are JavaScript’s universal data carrier — every array, function, class instance, and even modules are objects underneath. This chapter covers literals, access patterns, shorthand syntax, computed keys, and a first look at this.

Creating Objects

// Literal — by far the most common
const user = {
  name: "Ada",
  age:  37,
  active: true,
};

// Empty + assign
const empty = {};
empty.name = "Lin";

// Object.create — with a specific prototype
const fromProto = Object.create(null);   // no prototype at all

// Object constructor — rarely needed
const obj = new Object();

Tip: Use object literals ({}) 99% of the time. Object.create(null) is occasionally useful for “pure dictionaries” with no inherited methods.

Properties — Keys and Values

const user = {
  name: "Ada",                  // shorthand: key is "name", value is "Ada"
  "favourite color": "blue",    // quoted key — needed if key has spaces or starts with digit
  42: "the answer",             // numeric keys are coerced to strings
};

user.name                  // "Ada"
user["favourite color"]    // "blue"
user[42]                   // "the answer"
user["42"]                 // "the answer"  ← same key

Rules for unquoted keys: must be a valid identifier (letters, digits, _, $, no leading digit). Otherwise quote them.

Dot vs Bracket Access

const obj = { name: "Ada", "first-name": "Ada" };

obj.name              // "Ada"      ← dot
obj["name"]           // "Ada"      ← bracket, same thing
obj["first-name"]     // "Ada"      ← must use bracket (hyphen)
obj.first-name        // ❌ ReferenceError — parsed as obj.first - name

// Dynamic key from a variable
const key = "name";
obj[key];             // "Ada"
obj.key;              // undefined  ← literal property named "key"
Use dot Use bracket
Static, valid-identifier key Dynamic key (from a variable)
obj.name obj[someKey]
Most readable Required for spaces, hyphens, digits

Missing Properties

const obj = { name: "Ada" };

obj.age              // undefined  — no error
obj.profile.bio      // ❌ TypeError — can't read .bio of undefined

// Safe with optional chaining (ES2020)
obj.profile?.bio     // undefined  — short-circuits

// Default via nullish coalescing
obj.age ?? 0         // 0
obj.name ?? "n/a"    // "Ada"

// Checking existence
"name" in obj                // true   — includes inherited
obj.hasOwnProperty("name")   // true   — own only (older form)
Object.hasOwn(obj, "name")   // true   — own only (modern, ES2022)

Warning: Reading a missing property is silent (undefined); reading a property off undefined throws. Use ?. to traverse paths that might be incomplete.

Methods — Functions on Objects

const calc = {
  // Method shorthand (preferred)
  add(a, b) {
    return a + b;
  },
  // Equivalent classic form
  multiply: function (a, b) {
    return a * b;
  },
  // Arrow — beware: no own `this`
  greet: () => "hello",
};

calc.add(2, 3);        // 5
calc.multiply(4, 5);   // 20
calc.greet();          // "hello"

A First Look at this

Inside a regular function called as a method, this refers to the object the method was called on.

const user = {
  name: "Ada",
  greet() {
    return `Hi, I'm ${this.name}`;
  },
};

user.greet();   // "Hi, I'm Ada"

But this is dynamic — it depends on how the function is called, not where it was defined:

const greet = user.greet;
greet();        // "Hi, I'm undefined"   ← lost the binding

// Arrow methods don't get their own `this`
const broken = {
  name: "Ada",
  greet: () => `Hi, I'm ${this.name}`,
};
broken.greet(); // "Hi, I'm undefined"   ← `this` is from outer scope

Rule of thumb: method shorthand for object methods, arrow for callbacks. Full coverage in chapter 29.

Warning: Don’t write methods as arrow functions on object literals. They will not bind this to the object — they’ll capture the surrounding scope’s this.

Shorthand Property Names

When the variable name and key are the same:

const name = "Ada";
const age  = 37;

// Old
const user = { name: name, age: age };

// Shorthand (ES2015)
const user2 = { name, age };
// → { name: "Ada", age: 37 }

Computed Property Names

Use a bracket inside the literal to compute the key dynamically:

const field = "email";
const value = "ada@example.com";

const user = {
  name: "Ada",
  [field]: value,            // key is "email"
  [`is${"Active"}`]: true,   // key is "isActive"
};
// → { name: "Ada", email: "ada@example.com", isActive: true }

Very useful in reduce:

["a", "b", "c"].reduce((acc, k, i) => ({ ...acc, [k]: i }), {});
// { a: 0, b: 1, c: 2 }

Modifying, Adding, Deleting

const obj = { name: "Ada", age: 37 };

obj.age = 38;          // modify
obj.email = "a@b.com"; // add
delete obj.age;        // remove

console.log(obj);      // { name: "Ada", email: "a@b.com" }

delete is the only way to remove a property. (Setting to undefined keeps the key but with value undefined — different from absent.)

Iterating an Object

There is no forEach directly on objects. The standard idioms:

const user = { name: "Ada", age: 37, role: "admin" };

// Keys
Object.keys(user);        // ["name", "age", "role"]

// Values
Object.values(user);      // ["Ada", 37, "admin"]

// [key, value] pairs
Object.entries(user);     // [["name", "Ada"], ["age", 37], ["role", "admin"]]

// for…in — also walks inherited enumerable string keys
for (const key in user) {
  console.log(key, user[key]);
}

// Modern preferred form: destructure entries
for (const [key, value] of Object.entries(user)) {
  console.log(key, value);
}
Want Use
Keys as an array Object.keys(obj)
Values as an array Object.values(obj)
Pairs as an array Object.entries(obj)
Iterate with break for…of Object.entries(obj)

Tip: Property iteration order is insertion order for string keys, and integer-like keys come first in numeric order. Don’t rely on key order for correctness, but it is predictable.

Copying and Merging Objects

Both copy shallowly — nested objects are still shared by reference.

const original = { a: 1, b: 2 };

// Spread (most readable)
const copy   = { ...original };
const merged = { ...original, b: 99, c: 3 };
// → { a: 1, b: 99, c: 3 }

// Object.assign (mutates first argument)
const merged2 = Object.assign({}, original, { c: 3 });

Deep copy when needed:

// Modern
const deep = structuredClone(original);

// Older, JSON-safe only (loses functions, dates → strings, etc.)
const deepJson = JSON.parse(JSON.stringify(original));

Warning: Spread and Object.assign only copy the top level. {...a} does NOT clone nested objects — they remain shared. Reach for structuredClone for a real deep copy.

Object Destructuring (Preview)

const user = { name: "Ada", age: 37, role: "admin" };

// Extract
const { name, age } = user;
// name = "Ada", age = 37

// Rename
const { name: userName } = user;
// userName = "Ada"

// Default
const { country = "Unknown" } = user;

// Rest
const { name: n, ...rest } = user;
// n = "Ada", rest = { age: 37, role: "admin" }

Full chapter is 33.

Reference vs Value

Objects are passed by reference (technically, “by sharing”):

const a = { count: 1 };
const b = a;
b.count = 99;
console.log(a.count);   // 99   ← same object!

a === b;                // true
a === { count: 99 };    // false ← different object, even with same contents

// To compare contents you need a deep compare or JSON serialise
JSON.stringify(a) === JSON.stringify({ count: 99 });   // true (cheap & dirty)

A Realistic Example — Config Merger

const defaults = {
  retries: 3,
  timeoutMs: 5000,
  headers: { "Content-Type": "application/json" },
};

function fetchWith(overrides = {}) {
  const opts = {
    ...defaults,
    ...overrides,
    headers: { ...defaults.headers, ...(overrides.headers ?? {}) },
  };
  console.log(opts);
  // ...actual fetch
}

fetchWith({ retries: 5, headers: { "X-Trace": "abc" } });
// {
//   retries: 5,
//   timeoutMs: 5000,
//   headers: { "Content-Type": "application/json", "X-Trace": "abc" },
// }

Note the explicit deep merge of headers — a flat spread alone would have replaced the whole nested object.

Best Practices Checklist

  • ✅ Use object literals for almost every case
  • ✅ Use method shorthand (greet()) for object methods
  • ✅ Use shorthand and computed keys when they improve readability
  • ✅ Use Object.hasOwn(obj, key) (ES2022) to test for own properties
  • ✅ Use ?. and ?? to safely read possibly-missing values
  • ✅ Use structuredClone when you need a real deep copy
  • ❌ Don’t use arrow functions as methods that need this
  • ❌ Don’t rely on shallow spread to copy nested objects
  • ❌ Don’t iterate with for…in if you only want own keys — use Object.keys/entries

What’s Next?

You can build, read, and modify objects. Next, the static methods on Object — keys, values, entries, freeze, assign, fromEntries, and friends.

← Previous: JS Array Methods Next: JS Object Methods →

Updated: