A reference for the parts you reach for once a quarter
This is the fourth and final post on objects. It is a focused reference for the modern, less-frequently-used features that nonetheless matter when they matter: property descriptors, freeze/seal, symbols, the iterable protocol, structuredClone, Object.groupBy, and Proxy.
Series so far: basics · prototypes · classes.
Property descriptors
Every property on every object has a descriptor — a small record that controls how the property behaves.
1const user = { name: 'Ali' }
2Object.getOwnPropertyDescriptor(user, 'name')
3// { value: "Ali", writable: true, enumerable: true, configurable: true }
The four attributes:
| Attribute | Means |
|---|---|
value | The stored value (data descriptors only). |
writable | Can the value be reassigned with =? |
enumerable | Does it appear in for...in and Object.keys? |
configurable | Can the descriptor itself be changed or deleted? |
Accessor descriptors swap value/writable for get/set:
1Object.defineProperty(user, 'shouting', {
2 get() {
3 return this.name.toUpperCase()
4 },
5 enumerable: true,
6 configurable: true
7})
Object.defineProperty is the precision tool when you need a property that hides from iteration, is read-only, or is computed.
Freezing, sealing, preventing extension
Three levels of object lock-down:
| Method | Add props? | Remove props? | Reassign values? |
|---|---|---|---|
Object.preventExtensions(obj) | No | Yes | Yes |
Object.seal(obj) | No | No | Yes |
Object.freeze(obj) | No | No | No |
1const config = Object.freeze({ port: 8080 })
2config.port = 9090 // silently ignored (throws in strict mode)
Warning
All three are shallow.
Object.freeze({ nested: {} })does not freeze the inner object. Use a recursivedeepFreezehelper, or rely on the type system if you are in TypeScript.
Symbols
A Symbol is a unique, immutable primitive used as a property key. Symbol-keyed properties do not appear in Object.keys, Object.entries, or for...in — they are effectively hidden from accidental enumeration.
1const id = Symbol('id')
2const user = { name: 'Ali', [id]: 42 }
3
4Object.keys(user) // ["name"] ← symbol hidden
5user[id] // 42
Well-known symbols
The language uses symbols to expose hooks into built-in operations. The most useful:
| Symbol | Lets you override |
|---|---|
Symbol.iterator | How for...of, spread, and destructuring iterate. |
Symbol.asyncIterator | How for await...of iterates. |
Symbol.toPrimitive | How the object converts to string/number/default. |
Symbol.toStringTag | Result of Object.prototype.toString.call(obj). |
Symbol.hasInstance | How instanceof decides membership. |
1class Money {
2 constructor(amount, currency) {
3 this.amount = amount
4 this.currency = currency
5 }
6 [Symbol.toPrimitive](hint) {
7 if (hint === 'number') return this.amount
8 if (hint === 'string') return `${this.currency} ${this.amount}`
9 return `${this.amount}`
10 }
11}
12
13const m = new Money(50, 'USD')
14;+m // 50
15;`${m}` // "USD 50"
The iterable protocol
An object is iterable if it has a [Symbol.iterator]() method that returns an iterator — an object with .next() returning { value, done }.
1const range = {
2 from: 1,
3 to: 5,
4 [Symbol.iterator]() {
5 let current = this.from
6 const last = this.to
7 return {
8 next() {
9 return current <= last
10 ? { value: current++, done: false }
11 : { value: undefined, done: true }
12 }
13 }
14 }
15}
16
17;[...range] // [1, 2, 3, 4, 5]
18for (const n of range) {
19} // works natively
A generator is the simplest way to implement the protocol — see the modern functions post.
Cloning, comparing, grouping
structuredClone (ES2022, native)
The recommended deep-clone for plain data. Handles Date, Map, Set, RegExp, typed arrays, and circular references. Does not clone functions, DOM nodes, or class instances (the prototype is lost).
1const original = { tags: ['a', 'b'], created: new Date() }
2const copy = structuredClone(original)
Object.assign and spread
Both copy own enumerable string-keyed properties. Spread is the more idiomatic form and is preferred for new code.
Object.groupBy and Map.groupBy (ES2024)
Group an iterable by the value a callback returns:
1const orders = [
2 { id: 1, status: 'paid' },
3 { id: 2, status: 'pending' },
4 { id: 3, status: 'paid' }
5]
6
7Object.groupBy(orders, o => o.status)
8// { paid: [{id:1,...}, {id:3,...}], pending: [{id:2,...}] }
Map.groupBy is the same but uses a Map, so the keys can be any value (objects included).
Proxy and Reflect
A Proxy intercepts low-level operations on an object — get, set, delete, has, function call, construct. Use sparingly: every intercepted operation pays a runtime cost and confuses tools.
1const audited = new Proxy(
2 {},
3 {
4 set(target, key, value) {
5 console.log(`set ${key} = ${value}`)
6 return Reflect.set(target, key, value)
7 },
8 get(target, key) {
9 console.log(`get ${key}`)
10 return Reflect.get(target, key)
11 }
12 }
13)
14
15audited.name = 'Ali' // logs: set name = Ali
16audited.name // logs: get name
Reflect mirrors every trap-able operation — it is the recommended way to forward inside a handler.
Real-world uses include validation layers, deep-reactivity systems (Vue, MobX, Solid), and access loggers. They are not a substitute for normal getters/setters in everyday code.
Two patterns that show up everywhere
1. Configuration “with defaults”
1function configure(options) {
2 return Object.freeze({
3 method: 'GET',
4 timeout: 30_000,
5 retries: 3,
6 ...options // user wins
7 })
8}
2. Safe deep merge of plain JSON
1function mergeJSON(a, b) {
2 if (typeof a !== 'object' || a === null) return b
3 if (typeof b !== 'object' || b === null) return b
4 const out = Array.isArray(a) ? [...a] : { ...a }
5 for (const key of Object.keys(b)) {
6 out[key] = mergeJSON(a[key], b[key])
7 }
8 return out
9}
For anything more complex, reach for a battle-tested library — lodash.merge or deepmerge — rather than reinventing the wheel.
Summary
- Every property is described by
value/writable/enumerable/configurable(orget/set).Object.definePropertygives you full control. freeze,seal,preventExtensionsare shallow. Recurse for deep immutability.- Symbols give you collision-free hidden keys and the hooks (
Symbol.iterator,Symbol.toPrimitive, …) to integrate with language built-ins. - The iterable protocol makes any object work with
for...of, spread, destructuring, andPromise.all. structuredCloneis the modern deep clone;Object.groupBy/Map.groupBy(ES2024) collapse the classic “group an array” task into one call.Proxy+Reflectenable powerful meta-programming — use sparingly.
That completes the four-post object series. Next in the JavaScript track: Functional programming — the discipline of building programs as compositions of pure functions.










