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() { return this.name.toUpperCase(); },
3 enumerable: true,
4 configurable: true,
5});
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) { this.amount = amount; this.currency = currency; }
3 [Symbol.toPrimitive](hint) {
4 if (hint === "number") return this.amount;
5 if (hint === "string") return `${this.currency} ${this.amount}`;
6 return `${this.amount}`;
7 }
8}
9
10const m = new Money(50, "USD");
11+m; // 50
12`${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) {} // 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 set(target, key, value) {
3 console.log(`set ${key} = ${value}`);
4 return Reflect.set(target, key, value);
5 },
6 get(target, key) {
7 console.log(`get ${key}`);
8 return Reflect.get(target, key);
9 },
10});
11
12audited.name = "Ali"; // logs: set name = Ali
13audited.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.










