Objects, Prototypes & this —
The Object Model, Decoded.
You already write classes in Java and C#. You know inheritance. But JavaScript's model is
fundamentally different — it's built on prototypes, not classes, and
this
is bound at call time, not declaration time. This is the article that ends the confusion for good.
01 · Why the Object Model Matters
In Java, when you write class User, the compiler and JVM build a rigorous structure:
a vtable, a class loader, a metadata object. In JavaScript, class User is a lie.
It's syntactic sugar over a much older, weirder, more flexible mechanism:
prototypes.
The reason this matters isn't academic. Three of the most common production bugs in Node.js services trace directly to misunderstood object semantics:
Lost this
Passing a method as a callback silently breaks this. Express middleware, event handlers, and array callbacks all hit this.
Prototype Pollution
A malicious JSON payload can overwrite Object.prototype and poison every object in your app. Silent, dangerous, ubiquitous.
Shared Mutable State
Two services hold the same object reference. One mutates it. The other sees surprise changes. Debugging this eats entire days.
Fragile Inheritance
Deep class hierarchies where changing a base class breaks subclasses in unpredictable ways. Classic Liskov violations, made worse by JS flexibility.
02 · Objects — The Foundation
Everything in JavaScript that isn't a primitive (number, string,
boolean, null, undefined, symbol,
bigint) is an object. Functions are objects. Arrays are objects. Classes create
objects. Even Object.prototype is an object.
The Four Ways to Create an Object
// 1. Object literal (99% of the time this is what you want)
const user = { name: 'Alice', age: 30 };
// 2. new Object() — equivalent, but noisier. Avoid.
const user2 = new Object();
user2.name = 'Bob';
// 3. Object.create(proto) — creates an object with a specific prototype
const proto = { greet() { return 'hi'; } };
const obj = Object.create(proto);
obj.greet(); // 'hi' — inherited from proto
// 4. Constructor function (legacy pattern, still everywhere in old code)
function User(name) {
this.name = name;
}
const alice = new User('Alice');
Modern Object Syntax You Should Actually Use
const name = 'Alice';
const role = 'engineer';
const key = 'dynamic';
const user = {
// Shorthand property — name instead of name: name
name,
role,
// Computed property — [expression] as key
[key + 'Key']: 'value',
[`${role}-level`]: 5,
// Method shorthand
greet() { return `Hi ${this.name}`; },
// Getter / setter
get displayName() { return this.name.toUpperCase(); },
set displayName(v) { this.name = v.toLowerCase(); },
// Async method
async fetchProfile() { return fetch(`/users/${this.name}`); },
// Generator method
*tags() { yield 'admin'; yield role; }
};
console.log(user.dynamicKey); // 'value'
console.log(user['engineer-level']); // 5
Destructuring — The Backend Developer's Best Friend
If you're coming from Java, you may not realize how much JavaScript code lives in destructuring. It's not just sugar — it's the standard idiom for extracting request data, config, and function parameters.
const response = {
status: 200,
data: { id: 1, name: 'Alice', email: 'a@b.com' },
meta: { page: 1, total: 50 }
};
// Basic destructuring
const { status, data } = response;
// Nested destructuring
const { data: { name, email } } = response;
// Renaming — common when names conflict
const { data: userData, meta: pagination } = response;
// Default values
const { status: s = 500, headers = {} } = response;
// Rest — collect remaining properties
const { status: _, ...rest } = response;
// rest = { data: {...}, meta: {...} }
// Function parameters (this is how modern Express controllers look)
function createUser({ name, email, role = 'user', metadata = {} }) {
return { name, email, role, metadata };
}
// Combining with array destructuring in a params object
function paginate({ items, page = 1, size = 20 }) {
const start = (page - 1) * size;
return items.slice(start, start + size);
}
Killer pattern for backend: combine destructuring with defaults to define "shape contracts" without TypeScript:
function handle({ method = 'GET', path, headers = {}, body = null }) { … }
Now the function self-documents its inputs, and missing properties get safe defaults. It's TypeScript-lite, and it works in every runtime.
Spread Operator — Copy, Merge, Transform
const defaults = { retries: 3, timeoutMs: 5000, verbose: false };
const overrides = { timeoutMs: 10_000 };
// Merge with override precedence (order matters: last wins)
const config = { ...defaults, ...overrides };
// { retries: 3, timeoutMs: 10000, verbose: false }
// ⚠️ SHALLOW copy — nested objects are still shared references!
const original = { user: { name: 'Alice' }, tags: ['a'] };
const copy = { ...original };
copy.user.name = 'Bob';
console.log(original.user.name); // 'Bob' 😱 — same nested object!
// ✅ Deep clone for nested structure
const deepCopy = structuredClone(original);
deepCopy.user.name = 'Carol';
console.log(original.user.name); // 'Bob' — unchanged ✓
The shallow copy trap: { ...obj } is a shallow copy.
Nested objects and arrays remain shared. In a service layer that passes config around, one
careless mutation can affect every consumer. Use structuredClone() (native in
Node 17+) for a real deep clone.
03 · Property Descriptors & Metadata
Every property on a JavaScript object isn't just a key-value pair — it's a descriptor with four flags. You rarely need them, but when you do, knowing they exist turns you from "user" into "understander".
| Descriptor | Default | Meaning |
|---|---|---|
value | undefined | The property's value (for data descriptors) |
writable | true (literal) / false (defineProperty) | Can the value be reassigned? |
enumerable | true (literal) / false (defineProperty) | Does it appear in for...in and Object.keys? |
configurable | true (literal) / false (defineProperty) | Can it be deleted or redefined? |
get / set | — | Accessor functions (instead of value/writable) |
Defining Non-Enumerable, Read-Only Properties
const user = {};
// Define a read-only, non-enumerable, non-configurable ID
Object.defineProperty(user, 'id', {
value: 'user-abc123',
writable: false,
enumerable: false,
configurable: false
});
user.id = 'changed'; // silently fails (or throws in strict mode)
console.log(user.id); // 'user-abc123' — unchanged
console.log(Object.keys(user)); // [] — 'id' is not enumerable
// Inspect a property's descriptor
console.log(Object.getOwnPropertyDescriptor(user, 'id'));
// { value: 'user-abc123', writable: false, enumerable: false, configurable: false }
When You Actually Need This
In backend work, property descriptors show up in three real places:
Immutable IDs
Prevent accidental reassignment of primary keys or correlation IDs. Good defense against typos and library bugs.
ORM internals
Sequelize, Mongoose, TypeORM all use defineProperty to build computed and virtual fields.
Serialization control
Non-enumerable properties are automatically skipped by JSON.stringify. Great for internal metadata.
Object.freeze vs Object.seal vs Object.preventExtensions:
Object.freeze(obj) — all properties become non-writable, non-configurable.
Shallow only.
Object.seal(obj) — can't add/remove properties, but values remain writable.
Object.preventExtensions(obj) — can't add properties, but can delete/modify.
04 · The Prototype Chain — JavaScript's Real Inheritance
Forgetting classes for a moment: how does a JavaScript object find a property it
doesn't directly have? It looks at its prototype. If the prototype doesn't have it, the prototype's
prototype is checked. And so on, until Object.prototype, whose prototype is
null.
That's it. That's the whole inheritance model. Let's visualize it.
Object.create — The Purest Way
const animal = {
describe() { return `${this.name} is a ${this.kind}`; }
};
const dog = Object.create(animal);
dog.name = 'Rex';
dog.kind = 'dog';
dog.bark = function () { return 'Woof!'; };
// dog inherits from animal
console.log(dog.describe()); // "Rex is a dog"
console.log(dog.bark()); // "Woof!"
// Inspect the chain
console.log(Object.getPrototypeOf(dog) === animal); // true
console.log(Object.getPrototypeOf(animal) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null (end of chain)
Property Lookup — The Full Algorithm
dog own properties → not found
animal (dog's [[Prototype]]) → FOUND describe
this = dog
This is what people mean when they say "JavaScript doesn't have classes, it has prototypes."
When you access dog.describe, the engine walks this chain. When you call it,
this is set to dog — the receiver — not to animal.
The End of Every Chain
// Every plain object inherits from Object.prototype, which provides:
Object.prototype.toString() // '[object Object]'
Object.prototype.hasOwnProperty(x) // own-property check
Object.prototype.isPrototypeOf(o) // chain check
// And that's why this works on literally anything:
({}).toString(); // '[object Object]'
[] .toString(); // '' (arrays override toString)
(42).toString(); // '42' (via autoboxing to Number)
// 💡 Use Object.create(null) to get a "dictionary" with NO prototype
const dictionary = Object.create(null);
dictionary['hasOwnProperty'] = 'safe'; // no shadowing issues
console.log(dictionary.toString); // undefined — no inherited methods
Backend pattern: When you build lookup tables — caches, registries,
dictionaries — use Object.create(null). It gives you a truly empty object with no
inherited properties, so lookups like if (dict['constructor']) don't accidentally
return true because of inherited members. This is a real source of security bugs
in template engines.
05 · Prototype Pollution — A Real Security Threat
Before we move on, we need to talk about a real vulnerability class that catches backend teams
constantly. If you take user input and blindly merge it into an object, you can accidentally
overwrite Object.prototype — poisoning every object in your application.
// Attacker sends this JSON body to your API:
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
// Vulnerable code: naive recursive merge
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = target[key] || {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
merge({}, malicious);
// 💥 Now EVERY object in the process has isAdmin: true
console.log({}.isAdmin); // true 😱
console.log([] .isAdmin); // true
console.log(new Date().isAdmin); // true
Prototype pollution is a real CVE class. It has affected Lodash, jQuery, Mongoose, and hundreds of smaller libraries. Anywhere you merge untrusted objects into trusted ones — config merging, query building, template rendering — you're at risk.
How to Defend
// Layer 1: Block dangerous keys explicitly
const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (BLOCKED_KEYS.has(key)) continue;
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = safeMerge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Layer 2: Freeze the prototype (once, at startup)
Object.freeze(Object.prototype);
// Layer 3: Use Object.create(null) for user-data containers
const userProvided = Object.create(null);
Object.assign(userProvided, untrustedBody); // no prototype to pollute
// Layer 4: Validate with a schema library (Zod, Joi, Ajv)
// and reject unknown properties explicitly
Rule of thumb: Never iterate with for...in over untrusted
objects and merge blindly. Prefer Object.keys() and explicit allowlists. If you
rely on a library, check that it has patched prototype pollution CVEs.
06 · Classes — The Truth Behind the Sugar
When ES6 introduced class, a lot of backend developers breathed a sigh of relief.
"Finally, familiar syntax!" But it's important to know: class doesn't change
the object model. It's a thin veneer over prototypes. Let's prove it.
// "Modern" class syntax
class User {
constructor(name) { this.name = name; }
greet() { return `Hi ${this.name}`; }
}
// What it actually becomes (conceptually)
function UserFn(name) { this.name = name; }
UserFn.prototype.greet = function () {
return `Hi ${this.name}`;
};
// Proof: typeof class is "function"
console.log(typeof User); // 'function'
// Proof: the class body becomes the prototype
console.log(User.prototype.greet); // [Function: greet]
// Proof: instances are linked via prototype
const u = new User('Alice');
console.log(Object.getPrototypeOf(u) === User.prototype); // true
What Classes Actually Add Over Prototypes
| Feature | Classes | Old Function Pattern |
|---|---|---|
| Methods on prototype | Automatic | Manual |
| Non-enumerable methods | Default | Manual |
| Strict mode | Always on inside class body | Opt-in |
Calling without new | Throws error | Silently pollutes globals |
| Static methods | static keyword | Attach to function |
| Inheritance | extends + super | Manual Object.create |
| Private fields | #field | Closures (different semantics) |
| Getters/setters | get / set keywords | defineProperty |
| Hoisting | Not hoisted (TDZ) | Function declarations hoist |
The Full Modern Class — Every Feature
class Account {
// Static property — shared across all instances
static SUPPORTED_CURRENCIES = ['USD', 'EUR', 'GBP'];
// Private field — truly inaccessible from outside
#balance = 0;
#currency;
// Public class field
owner;
constructor(owner, currency = 'USD') {
if (!Account.SUPPORTED_CURRENCIES.includes(currency)) {
throw new Error(`Unsupported currency: ${currency}`);
}
this.owner = owner;
this.#currency = currency;
}
// Getter — reads like a property
get balance() { return this.#balance; }
get currency() { return this.#currency; }
deposit(amount) {
if (amount <= 0) throw new RangeError('Deposit must be positive');
this.#balance += amount;
return this.#balance;
}
withdraw(amount) {
if (amount > this.#balance) throw new Error('Insufficient funds');
this.#balance -= amount;
return this.#balance;
}
// Static method — factory
static from(json) {
const { owner, currency, balance } = json;
const acct = new Account(owner, currency);
if (balance > 0) acct.deposit(balance);
return acct;
}
// Instance method for serialization
toJSON() {
return { owner: this.owner, currency: this.#currency, balance: this.#balance };
}
}
// Usage
const acct = new Account('Alice', 'USD');
acct.deposit(100);
console.log(acct.toJSON()); // { owner: 'Alice', currency: 'USD', balance: 100 }
// acct.#balance // ❌ SyntaxError: private field
// JSON.stringify(acct) // ✅ uses toJSON()
Inheritance with extends & super
class SavingsAccount extends Account {
#interestRate;
constructor(owner, currency, interestRate = 0.02) {
super(owner, currency); // MUST call super before this
this.#interestRate = interestRate;
}
accrueInterest() {
const interest = this.balance * this.#interestRate;
return this.deposit(interest);
}
// Override + call super
toJSON() {
return { ...super.toJSON(), type: 'savings' };
}
}
const savings = new SavingsAccount('Bob', 'USD', 0.05);
savings.deposit(1000);
savings.accrueInterest();
console.log(savings.toJSON());
// { owner: 'Bob', currency: 'USD', balance: 1050, type: 'savings' }
The "super before this" rule is strict. In a subclass constructor, you cannot
reference this until super() has been called. Accessing this
first throws a ReferenceError. This catches a real class of bugs from the old
prototype pattern.
When NOT to Use Classes
Classes are great for stateful entities: accounts, connections, buffers, state machines. But they're not the answer for everything:
✅ Use classes when
- You have genuine state that changes over time (sessions, connections).
- You need instance methods bound to
this. - You want to encapsulate private fields (
#field). - You're wrapping a resource (DB pool, file handle).
- You're implementing a well-defined state machine.
❌ Avoid classes when
- You just need data — use plain objects (
{ name, age }). - You need pure functions — use free functions or modules.
- You'll serialize across boundaries — plain objects are safer.
- You only need one instance — use a module or singleton object.
- You need multiple behaviors — prefer composition (below).
07 · this — The Four Binding Rules
If there's one JavaScript concept that sends backend developers into existential doubt, it's
this. In Java or C#, this always means "the current instance". Period.
In JavaScript, this is determined at call time by how the function
is invoked. Not where it's defined. Not what class it belongs to.
The Four Rules (in Priority Order)
| # | Rule | Triggered By | this Becomes |
|---|---|---|---|
| 1 | new binding | new Foo() |
The newly created object |
| 2 | Explicit binding | fn.call(obj), fn.apply(obj), fn.bind(obj) |
The object passed in |
| 3 | Implicit binding | obj.fn() — called as a method |
The object to the left of the dot |
| 4 | Default binding | Just fn() — no context |
undefined (strict) / globalThis (sloppy) |
Rule 1 — new Binding
function Person(name) {
this.name = name;
this.greet = function () { return `Hi ${this.name}`; };
}
const p = new Person('Alice');
// Inside the constructor, this = the freshly created p object
console.log(p.greet()); // 'Hi Alice'
Rule 2 — Explicit Binding (call / apply / bind)
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
const alice = { name: 'Alice' };
const bob = { name: 'Bob' };
// .call(thisArg, ...args) — invoke immediately, individual args
console.log(greet.call(alice, 'Hello', '!')); // 'Hello, Alice!'
// .apply(thisArg, argsArray) — same but args in array
console.log(greet.apply(bob, ['Hi', '.'])); // 'Hi, Bob.'
// .bind(thisArg, ...boundArgs) — returns a new function
const greetAlice = greet.bind(alice, 'Hey');
console.log(greetAlice('?')); // 'Hey, Alice?'
Rule 3 — Implicit Binding (the "left of the dot" rule)
const counter = {
count: 0,
increment() {
this.count++;
return this.count;
}
};
counter.increment(); // this = counter (thing left of the dot)
console.log(counter.count); // 1
// ⚠️ Classic footgun: detaching the method
const detached = counter.increment;
detached(); // 💥 this = undefined (or globalThis in sloppy mode)
// TypeError: Cannot read property 'count' of undefined
Rule 4 — Default Binding
'use strict';
function standalone() {
return this;
}
console.log(standalone()); // undefined (strict)
// Without 'use strict':
// console.log(standalone()); // globalThis (window in browser, global in Node)
// In Node.js modules, strict mode is implicit — so you get undefined.
// This is why "Cannot read property 'x' of undefined" errors happen:
// a method was detached and this became undefined.
The 3-step diagnostic for any `this` question:
1. Was it called with new? → this is the new object.
2. Was it called with .call, .apply, or .bind? → this is the passed object.
3. Was it called as something.method()? → this is something.
Otherwise → undefined (strict) or global (sloppy).
08 · Arrow Functions & Lexical this
Arrow functions don't have their own this. They inherit it from the surrounding
scope — lexically. Once you understand this, the following puzzle becomes obvious.
const counter = {
count: 0,
// Regular function — this depends on CALL SITE
incrementRegular() {
setTimeout(function () {
this.count++; // 💥 this = undefined in strict mode
}, 100);
},
// Arrow function — this is lexically inherited from incrementArrow
incrementArrow() {
setTimeout(() => {
this.count++; // ✅ this = counter
}, 100);
}
};
Rule: Arrows Can't Be Rebinding
const obj = {
name: 'Alice',
greet: () => `Hi ${this.name}` // ⚠️ this = surrounding scope
};
console.log(obj.greet()); // "Hi undefined" — this is NOT obj!
// Arrow functions cannot be bound:
const arrow = () => this;
const bound = arrow.bind({ x: 1 });
console.log(bound()); // still the original lexical this — bind is ignored
Never use arrow functions as object methods. They look shorter but break
this. Use the method shorthand syntax instead.
❌ { greet: () => this.name }
✅ { greet() { return this.name; } }
When to Use Each — The Decision Tree
| Situation | Use | Why |
|---|---|---|
| Object method | method() { } | Needs dynamic this |
| Class method | method() { } | Same — inherited by subclasses |
Callback in .map() | (x) => x * 2 | No this needed |
Callback in setTimeout inside a method | () => {…} | Inherits outer this |
| Event handler for a class | Arrow in constructor or .bind() | Need the instance's this |
| Express middleware | async (req, res, next) => {…} | No this needed — arrows are fine |
| Higher-order factory | () => () => {…} | Concision |
The Express Middleware Trap
// ❌ Class method as Express handler — this is lost
class UserController {
async getUser(req, res) {
const user = await this.userService.find(req.params.id);
res.json(user);
}
}
const ctrl = new UserController();
app.get('/users/:id', ctrl.getUser);
// 💥 this is NOT the controller — Express calls it without context.
// ✅ Fix 1: bind in constructor
class UserController2 {
constructor(userService) {
this.userService = userService;
this.getUser = this.getUser.bind(this);
}
async getUser(req, res) { /* ... */ }
}
// ✅ Fix 2: use an arrow wrapper at the route
app.get('/users/:id', (req, res) => ctrl.getUser(req, res));
// ✅ Fix 3: define handlers as arrow class fields (bound per-instance)
class UserController3 {
constructor(userService) { this.userService = userService; }
getUser = async (req, res) => {
const user = await this.userService.find(req.params.id);
res.json(user);
};
}
// Now ctrl.getUser always has this = ctrl, no bind needed.
The modern idiom: define controller handlers as arrow class fields
(getUser = async (req, res) => {…}). This creates a per-instance bound function,
so this always points at the controller, no matter how the function is invoked.
It's the cleanest fix for the Express middleware trap.
09 · Composition Over Inheritance
Deep inheritance hierarchies are the classic "fragile base class" problem. Change a method in the base, break three subclasses in subtle ways. This isn't unique to JavaScript, but JavaScript gives you a beautiful alternative: composition.
The Inheritance Problem
class Animal {
move() { return 'moving'; }
eat() { return 'eating'; }
}
class Bird extends Animal {
move() { return 'flying'; }
layEgg() { return 'laying egg'; }
}
class Penguin extends Bird {
// Penguins can't fly, but they inherit 'flying' from Bird 😬
move() { return 'swimming'; } // override — but the hierarchy is a lie
}
// Now imagine changing Animal.move() — Bird.move overrides it.
// Change Bird.move — Penguin must remember to override again.
The Composition Alternative — Mixins
// Behaviors as standalone objects (capabilities)
const CanMove = {
move() { return this.movementStyle || 'moving'; }
};
const CanFly = {
fly() { return `${this.name} is flying`; }
};
const CanSwim = {
swim() { return `${this.name} is swimming`; }
};
const CanLayEggs = {
layEgg() { return `${this.name} laid an egg`; }
};
// Compose per-creature — no rigid hierarchy
const eagle = {
name: 'Eagle',
...CanMove, ...CanFly, ...CanLayEggs,
movementStyle: 'soaring'
};
const penguin = {
name: 'Penguin',
...CanMove, ...CanSwim, ...CanLayEggs,
movementStyle: 'waddling'
};
const dolphin = {
name: 'Dolphin',
...CanMove, ...CanSwim,
movementStyle: 'swimming'
};
console.log(eagle.fly()); // 'Eagle is flying'
console.log(penguin.swim()); // 'Penguin is swimming'
console.log(penguin.fly); // undefined — no such method
Function Composition — The Functional Approach
Even better than mixins for pure transformations: compose small functions.
// Pipe: data flows left-to-right through functions
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
// Compose: same, but right-to-left (mathematical style)
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
// Building blocks — tiny, focused, reusable
const trim = (s) => s.trim();
const lowercase = (s) => s.toLowerCase();
const normalizeEmail = pipe(trim, lowercase);
console.log(normalizeEmail(' Alice@Example.COM ')); // 'alice@example.com'
// Real backend pipeline: sanitize → validate → persist
const sanitize = (input) => ({ ...input, email: normalizeEmail(input.email) });
const validate = (data) => {
if (!data.email) throw new Error('Email required');
return data;
};
const withTimestamp = (data) => ({ ...data, createdAt: new Date().toISOString() });
const prepareUser = pipe(sanitize, validate, withTimestamp);
console.log(prepareUser({ email: ' Bob@X.COM ' }));
// { email: 'bob@x.com', createdAt: '2026-01-29T...' }
Backend principle: Prefer composition for stateless transformations (validation, sanitization, mapping) and classes for stateful resources (connections, pools, state machines). Both have their place. The mistake is using inheritance for everything because your university course did.
10 · Immutability Patterns
Immutable data is the single biggest source of stability in modern JavaScript. When objects never change, the entire class of "someone mutated something I was reading" bugs simply disappears. Let's build the toolkit.
Shallow Freeze — Fast and Common
const config = Object.freeze({
port: 3000,
host: '0.0.0.0'
});
config.port = 4000; // silently fails (or throws in strict)
config.newProp = 'x'; // silently fails
delete config.port; // silently fails
console.log(Object.isFrozen(config)); // true
// ⚠️ But shallow! Nested objects are still mutable:
const deep = Object.freeze({ db: { host: 'localhost' } });
deep.db.host = 'evil.com'; // 💥 works!
Deep Freeze — Production Ready
function deepFreeze(obj, seen = new WeakSet()) {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return obj; // handle circular refs
seen.add(obj);
for (const key of Reflect.ownKeys(obj)) {
deepFreeze(obj[key], seen);
}
return Object.freeze(obj);
}
const config = deepFreeze({
server: { port: 3000, host: '0.0.0.0' },
db: { url: 'postgres://...', pool: { min: 2, max: 10 } }
});
config.db.pool.max = 100; // ✅ silently ignored
console.log(config.db.pool.max); // 10 — unchanged
Immutable Updates — Spread + Override
const user = {
id: 1,
name: 'Alice',
settings: { theme: 'dark', locale: 'en' }
};
// ❌ Mutation — everyone holding a reference sees the change
user.settings.theme = 'light';
// ✅ Immutable update — new objects, old untouched
const updated = {
...user,
settings: { ...user.settings, theme: 'light' }
};
// Original is unchanged
console.log(user.settings.theme); // 'dark'
console.log(updated.settings.theme); // 'light'
console.log(user === updated); // false
console.log(user.settings === updated.settings); // false (new object)
console.log(user.name === updated.name); // true (unchanged props share reference — "structural sharing")
Structural sharing is the magic of immutable updates: unchanged branches of
the tree keep the same reference. This is why immutable data works so well with React's
reconciliation, and why === comparisons are enough to detect changes.
Immutability Helper — Readable Updates
// Generic "set at path" without mutating
function setIn(obj, path, value) {
const [head, ...rest] = path;
if (head === undefined) return value;
const current = obj?.[head];
const isArray = Array.isArray(current);
return {
...obj,
[head]: setIn(isArray ? [...current] : { ...current }, rest, value)
};
}
const state = {
user: { name: 'Alice', roles: ['user'] },
config: { verbose: false }
};
const next = setIn(state, ['user', 'roles', 1], 'admin');
// { user: { name: 'Alice', roles: ['user', 'admin'] }, config: {...} }
console.log(state.user.roles); // ['user'] — unchanged
console.log(next.user.roles); // ['user', 'admin']
✅ Immutability DO
- Freeze config objects at startup —
deepFreeze(config). - Use
structuredClone()to snapshot before mutating internally. - Return new objects from update functions.
- Use
Object.freezeon DTOs returned from services. - Prefer
Readonly<T>in TypeScript for service inputs.
❌ Immutability DON'T
- Don't deep-freeze performance-critical hot paths — it's slow.
- Don't freeze large arrays in request handlers.
- Don't assume
Object.freezeis deep. - Don't freeze objects you plan to extend (e.g., request objects).
- Don't freeze
Bufferor other native objects — behaviour varies.
11 · Symbols, Iterators & for...of
Two features that backend developers often skip over — but that unlock elegant patterns once
you know them. Symbols are unique keys. Iterators are how for...of works
behind the scenes.
Symbols — Truly Unique Keys
// Every Symbol() call produces a unique value
const ID = Symbol('id');
const ID2 = Symbol('id');
console.log(ID === ID2); // false — unique, despite same description
// Use symbols for internal metadata that must not collide with user keys
const INTERNAL_STATE = Symbol('internalState');
const user = {
name: 'Alice',
[INTERNAL_STATE]: { lastLogin: new Date() }
};
// Symbol keys don't appear in JSON.stringify, Object.keys, for...in
console.log(Object.keys(user)); // ['name']
console.log(JSON.stringify(user)); // {"name":"Alice"}
// Get all symbol keys explicitly:
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(internalState)]
// Well-known symbols let you customize language behavior
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
// Customize how Money + Money works
[Symbol.toPrimitive]() { return this.amount; }
// Customize how JSON.stringify sees it
toJSON() { return { amount: this.amount, currency: this.currency }; }
}
const price = new Money(100, 'USD');
console.log(price + 50); // 150 (uses Symbol.toPrimitive)
console.log(JSON.stringify(price)); // {"amount":100,"currency":"USD"}
Iterators & Generators — Custom for...of
for...of works on anything that has a [Symbol.iterator] method.
You can define your own iterables — perfect for pagination, streaming, or domain-specific traversal.
// Custom iterable: a BoundedRange
class BoundedRange {
constructor(from, to) { this.from = from; this.to = to; }
[Symbol.iterator]() {
let current = this.from;
const end = this.to;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
}
for (const n of new BoundedRange(1, 5)) {
console.log(n); // 1 2 3 4 5
}
// Same thing as a generator — much cleaner
function* boundedRange(from, to) {
for (let i = from; i <= to; i++) yield i;
}
for (const n of boundedRange(1, 5)) console.log(n);
Real Backend Pattern — Repository Pagination
class UserRepository {
async *iterateAll(pageSize = 100) {
let cursor = null;
while (true) {
const { items, nextCursor } = await this.fetchPage({ cursor, pageSize });
if (!items.length) break;
for (const item of items) yield item;
cursor = nextCursor;
if (!cursor) break;
}
}
async fetchPage({ cursor, pageSize }) {
// Real implementation would query the DB
return { items: [], nextCursor: null };
}
}
// Consume without loading everything into memory
const repo = new UserRepository();
for await (const user of repo.iterateAll(500)) {
await processUser(user);
}
Why this matters for backend: Async generators + for await...of
give you automatic backpressure. Each item is pulled only when the consumer is ready. You can
iterate billions of rows with constant memory. This is how streaming ETL, log processors, and
bulk importers are built.
12 · Production — Value Objects & Factories
Let's combine everything into a production-grade pattern you'll use for the rest of your career: the Value Object. It's how you replace primitive obsession with meaningful, self-validating types.
The Problem — Primitive Obsession
// 😱 Everything is a string — validations scattered everywhere
function createUser(email, phone, nationalId) {
if (!email.includes('@')) throw new Error('Bad email');
if (!/^\d{11}$/.test(phone)) throw new Error('Bad phone');
if (!/^\d{10}$/.test(nationalId)) throw new Error('Bad ID');
// ... and again in every other function that uses them
}
The Solution — Immutable Value Objects
// Base class — everything immutable, equality by value
class ValueObject {
equals(other) {
if (!other || other.constructor !== this.constructor) return false;
return JSON.stringify(this) === JSON.stringify(other);
}
toString() { return JSON.stringify(this); }
}
// Email — validates once, then trusted everywhere
class Email extends ValueObject {
#value;
constructor(value) {
super();
const normalized = String(value).trim().toLowerCase();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) {
throw new TypeError(`Invalid email: ${value}`);
}
this.#value = normalized;
Object.freeze(this);
}
get value() { return this.#value; }
get domain() { return this.#value.split('@')[1]; }
toJSON() { return this.#value; }
}
// Phone — Bangladesh format
class Phone extends ValueObject {
#value;
constructor(value) {
super();
const cleaned = String(value).replace(/\D/g, '');
if (!/^(8801|01)[3-9]\d{8}$/.test(cleaned)) {
throw new TypeError(`Invalid phone: ${value}`);
}
this.#value = cleaned.startsWith('01') ? '88' + cleaned : cleaned;
Object.freeze(this);
}
get value() { return this.#value; }
toJSON() { return this.#value; }
}
// Usage
const email = new Email(' ALICE@Example.COM ');
console.log(email.value); // 'alice@example.com'
console.log(email.domain); // 'example.com'
const phone = new Phone('01712-345678');
console.log(phone.value); // '8801712345678'
// Equality by value
const a = new Email('a@b.com');
const b = new Email('A@B.COM');
console.log(a.equals(b)); // true
console.log(a === b); // false (different instances — that's expected)
Factory Functions — Composable, Testable
// Factory returning a frozen, well-shaped User aggregate
function createUser({ email, phone, name, role = 'user' }) {
const user = {
id: crypto.randomUUID(),
email: new Email(email),
phone: new Phone(phone),
name: String(name).trim(),
role,
createdAt: new Date()
};
if (!user.name) throw new TypeError('Name required');
return Object.freeze(user);
}
// Usage — validation happens once, at creation
const user = createUser({
email: 'ALICE@Example.com',
phone: '01712345678',
name: 'Alice'
});
// Idempotent immutability — createUser returns frozen data
// user.role = 'admin'; // silently fails, no surprise
// Composite factories — small pieces compose up
function withAuditLog(entity, actor) {
return Object.freeze({
...entity,
auditLog: [...(entity.auditLog || []), { actor, at: new Date().toISOString() }]
});
}
Why Value Objects & Factories scale: Validation happens at construction. Everywhere else in the codebase, you can trust the data. No more "defensive" checks scattered across 20 functions. The boundary of trust is clean. This is the same discipline that makes TypeScript's branded types powerful — but it works in plain JavaScript.
13 · AI Corner for Refactoring
AI is phenomenal at mechanical refactoring — the tedious, risky kind that humans avoid. Here are the prompts that consistently produce useful output for the object model topics in this article.
Classes → Factories
"Refactor this class to a factory function that returns frozen plain objects. Preserve behavior. Keep private methods private via closure."
this-Binding Audit
"Find every method in this file that will lose `this` when passed as a callback. Show the failing call sites and the fix."
Prototype Pollution Scan
"Audit this merge utility for prototype pollution vulnerabilities. Suggest a hardened version using Object.create(null) and key allowlists."
Value Objects from Primitives
"Extract Email, Phone, and Money value objects from this codebase. Show the refactored service functions."
Inheritance → Composition
"Refactor this 4-level inheritance hierarchy into mixins or composition. Show the trade-offs."
Immutability Upgrade
"Convert this object's mutation methods into pure functions that return new instances. Preserve structural sharing where possible."
The most valuable prompt in this article: take any legacy class and ask: "Turn this into a value object — immutable, equality by value, validation at construction. Show me the refactored usage sites and any behavior changes." It's the single refactor that eliminates the largest class of "someone mutated my data" bugs.
Where AI gets the object model wrong: it often forgets that
Object.freeze is shallow, that { ...obj } doesn't deep clone, and that
arrow functions don't have their own this. Always test refactored output on real
code — don't trust the first response.
14 · Interactive Knowledge Check
Ten questions covering property descriptors, prototype chain, classes, this,
composition, and immutability. Take them seriously — they map directly to production scenarios.
Part 3 — Object Model Quiz
Ten questions on the real object model.15 · Cheat Sheet & What's Next
Object Model — One-Page Summary
| Concept | One-Line Rule |
|---|---|
| Object literal | The default. Use it for data. |
| Shorthand & computed | { name, [key]: v } — shorter, clearer. |
| Destructuring | The idiom for extracting request/config data. Use defaults. |
Spread ... | Shallow copy or merge. Not deep. Use structuredClone() for deep. |
| Property descriptors | writable/enumerable/configurable — rarely needed but sometimes essential. |
| Prototype chain | Property lookup walks up until null. That's inheritance. |
| Object.create(null) | Truly empty object — use for lookup tables. |
| Prototype pollution | Never merge untrusted input with __proto__, constructor, prototype. |
| class | Syntactic sugar over prototypes. Methods go on ClassName.prototype. |
| #private | Truly inaccessible fields. Use for encapsulation. |
| super | Must be called before this in subclass constructors. |
| this | Determined at call time: new → object, call/apply/bind → given, obj.fn() → obj, else undefined. |
| Arrow functions | No own this. Inherit lexically. Never use as object methods. |
| Mixins | Compose behaviors via { ...mixins }. Prefer over deep inheritance. |
| Object.freeze | Shallow immutability. Use a deep-freeze helper for trees. |
| Symbol | Unique keys, hidden from JSON, customizable via well-known symbols. |
| Iterators | Anything with [Symbol.iterator] works in for...of. |
| Async generators | Streaming with backpressure. Constant memory for any data size. |
| Value objects | Immutable, validated at construction, equality by value. |
| Factory functions | Cleaner than constructors for data — return frozen plain objects. |
Do / Don't — Object Model Edition
✅ DO
- Use object literals for data, classes for stateful resources.
- Freeze config and DTOs at their boundaries.
- Prefer composition and mixins over deep inheritance.
- Use arrow class fields for controller handlers.
- Use
Object.create(null)for lookup dictionaries. - Freeze
Object.prototypeonce at startup. - Use
structuredClone()for true deep copies. - Extract value objects from primitive-obsessed code.
- Use
[Symbol.iterator]for domain-specific traversal. - Trust your data after construction.
❌ DON'T
- Don't use arrow functions as object methods.
- Don't assume
Object.freezeis deep. - Don't assume
{ ...obj }is a deep copy. - Don't merge untrusted input blindly.
- Don't rely on
for...infor user-supplied objects. - Don't subclass "just because Java does it".
- Don't put shared mutable state on
Object.prototype. - Don't use
new Object()— use{}. - Don't forget
super()in subclass constructors. - Don't fight the prototype model — embrace it.
What's Coming in Part 4
Part 4 is about modern JavaScript syntax and modules — the features that make day-to-day JavaScript pleasant:
- ES Modules —
import/export, dynamic imports, tree shaking. - Optional chaining
?.and nullish coalescing??. - Logical assignment operators
||=,&&=,??=. - Top-level await — when it's a superpower, when it's a footgun.
- The new array methods:
.at(),.findLast(),Object.groupBy. - Generators in production — laziness, coroutines, and async iteration.
- WeakMap, WeakSet, FinalizationRegistry — real memory management in JavaScript.
- AI-assisted code migration from CommonJS to ESM.
Practice before Part 4: take one class from your codebase and convert it to
a factory function returning frozen objects. Then take one utility with heavy this
usage and convert it to arrow-friendly pure functions. Both exercises force you to think
about which model fits — and that thinking is where the growth happens.
Part 3 of 7 · JavaScript for Backend Developers · FreeLearning365.com
🌟 Continue Learning on FreeLearning365
Free tools, tutorials, and question banks for developers, students, and professionals.
- Learn Free ProgrammingJavaScript, Angular, Python, SQL, Data Analysis & More
- 100+ Free Online ToolsDevelopers, SEO Specialists & Daily Tasks
- Professional IT TrainingAdvance Your Career with Hands-On Courses
- AI Prompt Generator40+ Professional Prompt Types
- Drag & Drop Form GeneratorBootstrap 5.3/4, Custom CSS, Grid Layout
- Income Tax CalculatorNBR Slabs, Rebate & Minimum Tax
- NPS 2026 Salary Calculatorবাংলাদেশ জাতীয় বেতন স্কেল ২০২৬
- Electricity Bill CalculatorBERC Tariff & Appliance Report
- eBook CollectionFree for Download
- BCS / HSC / SSC Question Bankবাংলাদেশের সর্ববৃহৎ ফ্রি প্রশ্ন ব্যাংক
- AI Background RemoverRemove Image Background Free
- Free QR Code GeneratorCreate Custom QR Codes Online
- Barcode & Label GeneratorCustom Barcodes, QR Codes, A4 Sheets
- EV Class 9-10 All SubjectsPhysics, Chemistry, Biology, Math, ICT & BGS
- Our ServicesFull IT Solutions & Training
- Job Interview PreparationProgramming, Cloud, Data, ERP & More

0 Comments
thanks for your comments!