JavaScript for Backend Developers — Part 3: Objects, Prototypes, Classes & `this` — The Complete Object Model | FreeLearning365

JavaScript for Backend Developers — Part 3: Objects, Prototypes, Classes & `this` — The Complete Object Model | FreeLearning365

🚀 JavaScript for Backend Developers
🗺️ The Complete 7-Part Journey
Navigate anywhere in the series
Learning Path Part 1 of 7
🏆 Part 7 → AI + Observability + Production + 3 Capstone Projects

JavaScript for Backend Developers — Part 3: Objects, Prototypes, Classes & `this` — The Complete Object Model | FreeLearning365
Part 3 of 7 · JavaScript for Backend Developers

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.

📖 ~65 min deep read 🧪 7 interactive demos ⚡ 60+ code examples 🤖 AI-assisted refactoring

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.

🧭

The truth: JavaScript is a prototype-based language with class syntax bolted on for familiarity. Once you internalize the underlying model, class becomes a convenience — not a mystery. And this becomes predictable instead of chaotic.

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

creation.js JavaScript
// 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

modern-syntax.js JavaScript
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.

destructuring.js JavaScript
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

spread.js JavaScript
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
valueundefinedThe property's value (for data descriptors)
writabletrue (literal) / false (defineProperty)Can the value be reassigned?
enumerabletrue (literal) / false (defineProperty)Does it appear in for...in and Object.keys?
configurabletrue (literal) / false (defineProperty)Can it be deleted or redefined?
get / setAccessor functions (instead of value/writable)

Defining Non-Enumerable, Read-Only Properties

descriptors.js JavaScript
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.

Interactive — Prototype Chain Explorer

Object.create — The Purest Way

prototype-chain.js JavaScript
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

1 Check dog own properties → not found
2 Check animal (dog's [[Prototype]]) → FOUND describe
3 Return the function bound with 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

object-prototype.js — everything inherits from here JavaScript
// 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.

pollution.js — 💀 dangerous JavaScript
// 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

safe-merge.js — three layers of defense JavaScript
// 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.

class-is-sugar.js JavaScript
// "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 prototypeAutomaticManual
Non-enumerable methodsDefaultManual
Strict modeAlways on inside class bodyOpt-in
Calling without newThrows errorSilently pollutes globals
Static methodsstatic keywordAttach to function
Inheritanceextends + superManual Object.create
Private fields#fieldClosures (different semantics)
Getters/settersget / set keywordsdefineProperty
HoistingNot hoisted (TDZ)Function declarations hoist

The Full Modern Class — Every Feature

full-class.js JavaScript
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

inheritance.js JavaScript
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 actor metaphor: this is like a role in a play. The same script (function) can be performed by different actors (objects) on different nights. The script doesn't know who will play the role — the director (the call site) decides.

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

rule-new.js JavaScript
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)

rule-explicit.js JavaScript
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)

rule-implicit.js JavaScript
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
Interactive — this Binding Visualizer

Rule 4 — Default Binding

rule-default.js JavaScript
'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.

arrow-this.js JavaScript
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

arrow-cannot-rebind.js JavaScript
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 methodmethod() { }Needs dynamic this
Class methodmethod() { }Same — inherited by subclasses
Callback in .map()(x) => x * 2No this needed
Callback in setTimeout inside a method() => {…}Inherits outer this
Event handler for a classArrow in constructor or .bind()Need the instance's this
Express middlewareasync (req, res, next) => {…}No this needed — arrows are fine
Higher-order factory() => () => {…}Concision

The Express Middleware Trap

express-middleware.js JavaScript
// ❌ 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

inheritance-fragility.js JavaScript
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

mixins.js JavaScript
// 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.js JavaScript
// 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

freeze.js JavaScript
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

deep-freeze.js JavaScript
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

immutable-updates.js JavaScript
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

update-in.js JavaScript
// 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.freeze on 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.freeze is deep.
  • Don't freeze objects you plan to extend (e.g., request objects).
  • Don't freeze Buffer or 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

symbols.js JavaScript
// 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-iterables.js JavaScript
// 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

paginate-generator.js JavaScript
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

primitive-obsession.js JavaScript
// 😱 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

value-objects.js JavaScript
// 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

factories.js JavaScript
// 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.
Score: 0 / 10

15 · Cheat Sheet & What's Next

Object Model — One-Page Summary

Concept One-Line Rule
Object literalThe default. Use it for data.
Shorthand & computed{ name, [key]: v } — shorter, clearer.
DestructuringThe idiom for extracting request/config data. Use defaults.
Spread ...Shallow copy or merge. Not deep. Use structuredClone() for deep.
Property descriptorswritable/enumerable/configurable — rarely needed but sometimes essential.
Prototype chainProperty lookup walks up until null. That's inheritance.
Object.create(null)Truly empty object — use for lookup tables.
Prototype pollutionNever merge untrusted input with __proto__, constructor, prototype.
classSyntactic sugar over prototypes. Methods go on ClassName.prototype.
#privateTruly inaccessible fields. Use for encapsulation.
superMust be called before this in subclass constructors.
thisDetermined at call time: new → object, call/apply/bind → given, obj.fn() → obj, else undefined.
Arrow functionsNo own this. Inherit lexically. Never use as object methods.
MixinsCompose behaviors via { ...mixins }. Prefer over deep inheritance.
Object.freezeShallow immutability. Use a deep-freeze helper for trees.
SymbolUnique keys, hidden from JSON, customizable via well-known symbols.
IteratorsAnything with [Symbol.iterator] works in for...of.
Async generatorsStreaming with backpressure. Constant memory for any data size.
Value objectsImmutable, validated at construction, equality by value.
Factory functionsCleaner 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.prototype once 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.freeze is deep.
  • Don't assume { ...obj } is a deep copy.
  • Don't merge untrusted input blindly.
  • Don't rely on for...in for 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.

🌍 FreeLearning365.com — Your gateway to free learning, tools & resources.

Part 3 of 7 · JavaScript for Backend Developers · © 2026 FreeLearning365


🚀 JavaScript for Backend Developers
🗺️ The Complete 7-Part Journey
Navigate anywhere in the series
Learning Path Part 1 of 7
🏆 Part 7 → AI + Observability + Production + 3 Capstone Projects

Post a Comment

0 Comments