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,
invsObject.hasOwnES2022, and the property key order. Object.keys/values/entriesES2017 andObject.fromEntriesES2019.- 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
obj.nameworks for valid identifiers.obj["any key"]works for any string, symbol or variable.- Property keys are always strings or symbols:
obj[1]andobj["1"]are the same property. - Reading a missing property gives
undefined; reading throughundefinedthrows, which is why?.exists. delete obj.keyremoves an own property.
| Check | Own properties | Inherited properties |
|---|---|---|
"key" in obj | β | β |
Object.hasOwn(obj, "key") ES2022 | β | β |
obj.key !== undefined | wrong 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:
- Integer-like keys (
"0","42") first, in ascending numeric order; - then other string keys, in insertion order;
- 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:
enumerable: shows up inObject.keys,forβ¦in, spread and JSON;configurable: can be deleted or redefined.
Properties created by assignment or literals have all flags true. Properties created with Object.defineProperty default to false.
Locking objects down
| Function | Add properties | Delete properties | Change values | Check 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:
| Method | Own? | 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
| Technique | Depth | Keeps | Loses or breaks |
|---|---|---|---|
{ ...obj }, Object.assign({}, obj) | shallow | own enumerable props and symbols | prototype, getters (copied as their current value), non-enumerable props |
structuredClone(obj) | deep | Dates, Maps, Sets, typed arrays, cycles | prototype/class, getters; throws on functions |
JSON.parse(JSON.stringify(obj)) | deep | plain JSON data | Dates 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.
insees inherited keys andObject.hasOwnsees only own ones. entriesβmapβfromEntriesis the idiom for transforming objects.- Descriptors control
writable/enumerable/configurable, anddefinePropertydefaults them to false. Freeze, seal and preventExtensions are shallow. - Spread copies own enumerable keys shallowly;
structuredClonecopies 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.