Chapter 10 Β· Collections & Objects

Working with Objects

Objects are bags of properties, but each property carries hidden flags, keys have a defined order, and every way of listing or copying properties sees a slightly different subset. Knowing these rules explains many "where did my key go?" bugs.

You will learn

  • Modern object literals: shorthand properties and methods, computed keys, getters and setters ES2015.
  • Property access, in vs Object.hasOwn ES2022, and the property key order.
  • Object.keys/values/entries ES2017 and Object.fromEntries ES2019.
  • Property descriptors (writable, enumerable, configurable), plus freeze and seal.
  • Enumerability and ownership: what each listing and copying method sees; shallow vs deep copies.

Object literals, the modern way ES2015

Accessing properties

CheckOwn propertiesInherited properties
"key" in objβœ“βœ“
Object.hasOwn(obj, "key") ES2022βœ“βœ—
obj.key !== undefinedwrong if the value is undefinedβœ“

πŸ“ Older syntax you'll still see: obj.hasOwnProperty("key") and Object.prototype.hasOwnProperty.call(obj, "key"). Object.hasOwn replaces both, and it also works on null-prototype objects.

Property key order

Object keys are listed in a defined order, and it's not always insertion order:

  1. Integer-like keys ("0", "42") first, in ascending numeric order;
  2. then other string keys, in insertion order;
  3. then symbols, in insertion order (only for methods that include symbols).

("-1" and "01" aren't canonical array indexes, so they count as ordinary strings.) If you need guaranteed insertion order for number-like keys, use a Map.

Keys, values, entries

Property descriptors

Every property has a descriptor: either a data descriptor (value and writable) or an accessor descriptor (get and set), plus two flags:

Properties created by assignment or literals have all flags true. Properties created with Object.defineProperty default to false.

Locking objects down

FunctionAdd propertiesDelete propertiesChange valuesCheck with
Object.preventExtensionsβœ—βœ“βœ“Object.isExtensible
Object.sealβœ—βœ—βœ“Object.isSealed
Object.freezeβœ—βœ—βœ—Object.isFrozen

All three are shallow: nested objects stay mutable (see deep freeze in Chapter 2).

Enumerability and ownership

Each way of listing or copying properties sees a different slice. This table, from MDN's "Enumerability and ownership" page, is worth bookmarking:

MethodOwn?Inherited?Non-enumerable?Symbols?
Object.keys / values / entriesβœ“βœ—βœ—βœ—
for…inβœ“βœ“βœ—βœ—
Object.getOwnPropertyNamesβœ“βœ—βœ“βœ—
Object.getOwnPropertySymbolsβœ“βœ—βœ“only symbols
Reflect.ownKeysβœ“βœ—βœ“βœ“
Spread {...o} / Object.assignβœ“βœ—βœ—βœ“
JSON.stringifyβœ“βœ—βœ—βœ—

Copying and comparing

TechniqueDepthKeepsLoses or breaks
{ ...obj }, Object.assign({}, obj)shallowown enumerable props and symbolsprototype, getters (copied as their current value), non-enumerable props
structuredClone(obj)deepDates, Maps, Sets, typed arrays, cyclesprototype/class, getters; throws on functions
JSON.parse(JSON.stringify(obj))deepplain JSON dataDates become strings; undefined, functions, Map/Set and BigInt are dropped or throw

There's no built-in deep equality for objects: === compares identity. In Node, util.isDeepStrictEqual(a, b) works; in tests, use assert.deepStrictEqual (Appendix B). Don't compare JSON.stringify outputs, because key order makes equal objects look different.

More examples

1. Picking and omitting keys

2. A dictionary with no inherited surprises

3. Computed getters for derived data

4. Merging defaults deeply (one level)

5. Inverting an object

Recap

  • Shorthand properties and methods, computed keys, and getters/setters make literals expressive.
  • Keys are strings or symbols; integer-like keys are listed first, in numeric order. in sees inherited keys and Object.hasOwn sees only own ones.
  • entries β†’ map β†’ fromEntries is the idiom for transforming objects.
  • Descriptors control writable/enumerable/configurable, and defineProperty defaults them to false. Freeze, seal and preventExtensions are shallow.
  • Spread copies own enumerable keys shallowly; structuredClone copies deeply but drops classes and throws on functions.

Exercises

Exercise 1 🟒 easy refactor it

Modernize: var o = { name: name, price: price, show: function () { return this.name; } };

Show solution

Use shorthand properties, and method shorthand, which is still a regular function with its own this.

Exercise 2 🟒 easy predict the output

What is Object.keys({ z: 1, 3: 1, a: 1, 1: 1 })?

Show solution

[ '1', '3', 'z', 'a' ]. Integer-like keys come first in ascending order, then the other strings in insertion order.

Exercise 3 🟒 easy fix the bug

This says an order with discount: 0 has no discount field. Fix the check.

Show solution

Truthiness tests the value. To ask whether the property exists, use Object.hasOwn (or in, which also counts inherited keys).

Exercise 4 🟒 easy build it

Turn { dune: 1299, emma: 899 } into { dune: "€12.99", emma: "€8.99" } using entries and fromEntries.

Show solution

This round trip is the standard way to "map over an object".

Exercise 5 🟑 medium predict the output

What does this print?

Show solution

11 2 undefined. Spread read the getter once and stored its value 2 as a plain data property; the copy has no getter.

Exercise 6 🟑 medium build it

Add a read-only, non-enumerable createdAt property to an order object, so that it's hidden from JSON.stringify but still readable.

Show solution

defineProperty defaults writable, enumerable and configurable to false.

Exercise 7 🟑 medium build it · playground

Create playground/diff.js exporting diff(before, after), which returns { added, removed, changed } lists of keys (own keys only, compared with Object.is).

Show solution

Object.is treats NaN as equal to itself, so an unchanged NaN isn't reported. With !==, it would be.

Exercise 8 🟑 medium explain why

Why is JSON.stringify(a) === JSON.stringify(b) a bad deep-equality check?

Show solution

Key order matters to stringify, so {a:1,b:2} and {b:2,a:1} look different. It also drops undefined and functions, turns NaN into null, and can't handle Maps, Sets, BigInt or cycles. Use util.isDeepStrictEqual or a tested library.

Exercise 9 πŸ”΄ hard predict the output

What does this print?

Show solution

[ 'visible', 'shared' ] [ 'visible' ] true false. for…in includes inherited enumerable keys but not the non-enumerable secret; in finds secret anyway; shared isn't an own property.

Exercise 10 πŸ”΄ hard mini-project Β· playground

Create playground/model.js exporting defineModel(fields). For example, defineModel({ title: "string", price: "number" }) returns a create(data) function that makes a sealed object with validating setters for each field, a non-enumerable _errors array, and a toJSON that outputs only the fields.

Show solution

Each field gets an accessor descriptor whose setter validates; Object.assign runs those setters. _errors and toJSON are non-enumerable, so they stay out of JSON, and toJSON's spread reads the getters. Object.seal blocks new properties, and because modules are strict, the attempt throws instead of failing silently.

Official docs