Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 8

Objects and Properties

JavaScript objects are the language's central data structure: dynamic bags of key–value pairs. But beneath the simple `obj.prop` syntax sits a precise machinery of descriptors, accessors, extensibility flags, and prototype delegation. This chapter assumes you are already comfortable with values, typ
Level: AdvancedLength: In depth4 min read

Read at another depth

Objects and Properties

JavaScript objects are the language's central data structure: dynamic bags of key–value pairs. But beneath the simple obj.prop syntax sits a precise machinery of descriptors, accessors, extensibility flags, and prototype delegation. This chapter assumes you are already comfortable with values, types, functions, and closures, and have at least passing familiarity with the fact that a prototype chain exists (a later chapter treats it fully). Here we focus on the object itself: what a property actually is, and how to control one.

Property Kinds

An object has two kinds of property:

  1. Data properties store a value.
  2. Accessor properties are a getter and/or setter function pair.

Internally, every property is described by a property descriptor with these fields:

  • value and writable (data properties only)
  • get and set (accessor properties only)
  • enumerable — whether it shows up in for...in and Object.keys
  • configurable — whether the descriptor can be changed or the property deleted

Inspect descriptors directly:

Code example in javascript
const obj = { a: 1 };

console.log(Object.getOwnPropertyDescriptor(obj, 'a'));
// { value: 1, writable: true, enumerable: true, configurable: true }

Object.defineProperty(obj, 'b', {
  get() { return 42; },
  enumerable: true,
});

console.log(obj.b); // 42

The cost of a single misstep

defineProperty fails silently or throws depending on strict mode, and configurable: false is irreversible. Once you set it, the only change you can still make is writable: true → false. Nothing else — not re-defining, not deleting.

The Four Mutation Levels

Objects have their own meta-level controls, layered on top of per-property flags:

MethodPreventsObjects still extensible?Existing props writable?
`Object.preventExtensions`Adding new propertiesNoYes
`Object.seal`Adding/removing; properties become non-configurableNoYes
`Object.freeze`Everything: also makes properties non-writableNoNo

freeze is shallow: nested objects remain mutable. Use a recursive freeze (or a library) if you need deep immutability, but be aware it also freezes functions and can be expensive on hot paths.

Code example in javascript
const state = Object.freeze({ user: { name: 'Ada' } });
state.user.name = 'Grace';   // silently ignored (or TypeError in strict mode)
console.log(state.user.name); // 'Grace' — the nested object was never frozen

Getters, Setters, and Invariants

Accessor properties are declared in object literals or classes:

Code example in javascript
const account = {
  #balance: 0,           // private field (class fields syntax)
  get balance() { return this.#balance; },
  set balance(v) {
    if (typeof v !== 'number' || v < 0) throw new RangeError('bad balance');
    this.#balance = v;
  },
};

Key rule: accessors bypass writable. If you freeze an object containing accessors, the getter still runs — but any setter that mutates captured state via closure (rather than this) will happily "violate" the freeze. Freezing constrains the property table, not the functions themselves.

Also note: a setter with no getter makes the property read undefined — and in strict mode, reading it is fine, but the asymmetry often surprises readers of your code.

Symbols as Property Keys

Object keys are strings or symbols — unique, opaque values useful for "hidden-ish" properties:

Code example in javascript
const id = Symbol('id');
const obj = { [id]: 123, name: 'obj' };

console.log(Object.keys(obj));       // ['name'] — symbols excluded
console.log(Reflect.ownKeys(obj));   // ['name', Symbol(id)]

Object.keys and JSON.stringify skip symbol keys; Object.assign and spread copy them. This asymmetry is a frequent source of serialization bugs: symbols survive cloning but not stringification.

Property Access: The Full Path

obj.prop is not a table lookup. It:

  1. Checks for an own accessor property → calls the getter.
  2. Checks for an own data property → returns the value.
  3. Walks the prototype chain, repeating 1–2.
  4. Returns undefined if nothing is found.

This is why reading a property is never guaranteed side-effect-free: a getter anywhere in the chain can execute arbitrary code. In performance-sensitive code, a monomorphic shape (consistent set of keys, no accessors) lets engines optimize access into inline cache loads; polymorphic access sites or getters deoptimize.

Copying and Merging Correctly

Spread and Object.assign perform shallow copies and copy via the [[Set]] operation — meaning they call setters on the target. To copy raw values instead, define with descriptors:

Code example in javascript
function copyDescriptors(src) {
  return Object.defineProperties({}, Object.getOwnPropertyDescriptors(src));
}

Note the difference: {...src} reads through getters and writes through setters; getOwnPropertyDescriptors copies the accessor functions themselves. Choose based on whether you want behavior or snapshot.

Common Errors and Gotchas

  • undefined vs. missing: 'x' in obj is true even for obj.x = undefined. Use in for presence, Object.hasOwn(obj, 'x') (ES2022) to exclude the prototype chain. Never trust truthiness — obj.x is falsy for 0, '', and false.
  • Mutating shared references: assignment copies the reference, not the object. {...a} nested two levels deep is not a safe copy.
  • Frozen ≠ deeply immutable (shown above), and frozen objects cannot gain hidden-class-fast new properties later — freezing too early can hurt performance.
  • Ordering: string keys iterate in insertion order, except integer-like keys, which iterate in ascending numeric order first. const o = { 3: 'c', 1: 'a', b: 'z' } iterates 1, 3, b.
  • delete deoptimizes: it changes the object's hidden class. Prefer setting to undefined (and documenting) in hot code, or use Map when keys are dynamic.

Exercise

You freeze an object whose property is a getter reading from a closed-over variable. Can you still change the value the getter returns?

Show answer

Yes. Freezing prevents descriptor changes and property addition/removal, but the getter is just a function with closure state; mutating that captured variable is unaffected by the freeze.

Exercise

Why does { ...obj } differ from Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj)) when obj has a getter?

Show answer

Spread performs a [[Get]] on obj (running the getter) and a [[Set]] of the resulting value, producing a data property. The descriptors version copies the accessor itself, so the new object's getter runs against the new object when read.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Objects and Properties

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that