Module 1 established that objects live on the Heap and are accessed by pointer. This module opens the object up: what a property actually is beneath its value, how objects delegate to other objects to form inheritance, and how the four-rules-of-this and the class keyword are just ergonomic layers over that same delegation mechanism.


1. Objects in Memory: Property Descriptors

A property is not just a key-value pair. Internally, every property on an object is backed by a Property Descriptor -- a small metadata record controlling how that property behaves. There are two kinds: data descriptors (value + writable) and accessor descriptors (get/set). Both share two universal flags: configurable and enumerable.

What each descriptor flag actually controls

  • value: the actual data stored (data descriptors only)
  • writable: if false, assignment to the property silently fails in non-strict mode and throws a TypeError in strict mode -- the property becomes read-only
  • enumerable: if false, the property is invisible to for...in, Object.keys(), and JSON.stringify(), but still directly accessible by name
  • configurable: if false, the property cannot be deleted, and its descriptor cannot be changed again (with one exception: writable can still be flipped from true to false)
  • get / set: functions that intercept reads and writes instead of storing a static value (accessor descriptors only -- mutually exclusive with value/writable)
property-descriptors.jsjavascript
const config = {};

// Object literal properties default to { writable: true, enumerable: true, configurable: true }
Object.defineProperty(config, 'apiVersion', {
  value: 'v2',
  writable: false,    // read-only
  enumerable: true,
  configurable: false // cannot be deleted or redefined
});

config.apiVersion = 'v3';           // silently ignored (non-strict) or throws (strict)
console.log(config.apiVersion);     // 'v2' -- unchanged

delete config.apiVersion;           // fails -- configurable: false
console.log(config.apiVersion);     // still 'v2'

// Accessor descriptor: computed on read, validated on write
let _internalCount = 0;
Object.defineProperty(config, 'count', {
  get() { return _internalCount; },
  set(next) {
    if (typeof next !== 'number' || next < 0) {
      throw new TypeError('count must be a non-negative number');
    }
    _internalCount = next;
  },
  enumerable: true,
  configurable: false
});

config.count = 5;
console.log(config.count); // 5
// config.count = -1;      // throws TypeError -- validation runs on every write

2. Prototypes & Inheritance: Delegation, Not Classes

JavaScript has no classical inheritance at the engine level -- it has prototypal delegation. Two distinct things are easily confused: prototype is a plain property that exists ONLY on functions (specifically, on functions usable as constructors), holding the object that will become the [[Prototype]] of instances created with new. __proto__ is a legacy accessor exposing an object's actual internal [[Prototype]] link -- the object it delegates to when a lookup fails locally.

How property lookup actually walks the chain

  • When you access obj.prop, the engine first checks obj's OWN properties
  • If not found, it follows obj's internal [[Prototype]] link (exposed as __proto__) to the next object in the chain and checks there
  • This repeats until the property is found, or until [[Prototype]] is null (the end of every chain -- Object.prototype's own [[Prototype]] is null)
  • This is called Behavior Delegation: an object doesn't 'inherit' methods by copying them -- it delegates the lookup to another live object, so a change to the prototype object is instantly visible to every object delegating to it
  • Own properties always shadow (take priority over) delegated ones with the same name -- this is how you 'override' a method without touching the prototype
manual-prototype-chain.jsjavascript
// Building inheritance BY HAND, with zero use of `class`, to expose the mechanics.

const animalBehaviors = {
  // `this` here is determined by the CALL SITE, not by where this function is defined
  // -- see section 3. When called as dog.describe(), `this` is `dog`.
  describe() {
    return `${this.name} makes a sound: ${this.speak()}`;
  },
  speak() {
    return '...';
  }
};

const dogBehaviors = Object.create(animalBehaviors); // dogBehaviors.__proto__ === animalBehaviors
dogBehaviors.speak = function () { return 'Woof'; };  // shadows animalBehaviors.speak

function createDog(name) {
  const dog = Object.create(dogBehaviors); // dog.__proto__ === dogBehaviors
  dog.name = name;
  return dog;
}

const rex = createDog('Rex');
console.log(rex.describe()); // 'Rex makes a sound: Woof'

// The chain: rex -> dogBehaviors -> animalBehaviors -> Object.prototype -> null
console.log(Object.getPrototypeOf(rex) === dogBehaviors);              // true
console.log(Object.getPrototypeOf(dogBehaviors) === animalBehaviors);  // true
console.log(rex.hasOwnProperty('name'));    // true  -- own property
console.log(rex.hasOwnProperty('speak'));   // false -- delegated, found via the chain

3. The this Keyword: Four Binding Rules

this is not determined by where a function is defined -- it is determined by HOW the function is called (the call-site), except for arrow functions. There are exactly four rules, and they have a strict precedence order when more than one could apply.

RuleTriggerthis resolves to
New BindingCalled with new Fn()The newly created object (highest precedence)
Explicit BindingCalled via .call(obj), .apply(obj), or .bind(obj)The object passed as the first argument
Implicit BindingCalled as a method: obj.method()The object the method was called on (obj)
Default BindingCalled as a bare function: fn()undefined in strict mode; the global object in non-strict mode
this-binding-rules.jsjavascript
'use strict';

function whoAmI() { return this; }

// 4. Default Binding -- bare call, strict mode -> undefined
console.log(whoAmI());                 // undefined

// 3. Implicit Binding -- called AS A METHOD of user
const user = { name: 'Ada', whoAmI };
console.log(user.whoAmI().name);       // 'Ada'

// A classic trap: extracting the method loses its receiver, falling back to Default Binding
const detached = user.whoAmI;
console.log(detached());               // undefined -- `this` is NOT `user` anymore

// 2. Explicit Binding -- call/apply/bind force `this` regardless of call-site
function greet(greeting) { return `${greeting}, ${this.name}`; }
console.log(greet.call({ name: 'Grace' }, 'Hello'));   // 'Hello, Grace'
const boundGreet = greet.bind({ name: 'Linus' });
console.log(boundGreet('Hi'));                          // 'Hi, Linus'

// 1. New Binding -- highest precedence, even overrides a prior .bind()
function Person(name) { this.name = name; }
const BoundPerson = Person.bind({ name: 'Ignored' });
const p = new BoundPerson('Real Name'); // `new` wins -- creates a fresh object
console.log(p.name);                    // 'Real Name'
arrow-lexical-this.jsjavascript
// BAD WAY (Don't Do This): losing `this` in a callback with a regular function
class Timer {
  constructor() { this.seconds = 0; }
  startBad() {
    setInterval(function () {
      this.seconds++; // `this` here is Default Binding (undefined/global) -- NOT the Timer instance
    }, 1000);
  }
}

// ARCHITECT WAY (Do This): arrow function captures `this` lexically from startGood()
class TimerFixed {
  constructor() { this.seconds = 0; }
  startGood() {
    setInterval(() => {
      this.seconds++; // `this` is resolved via the Scope Chain -> TimerFixed instance
    }, 1000);
  }
}

4. Modern ES6+ Classes: Syntactic Sugar Over Prototypes

class did not add a new inheritance model to JavaScript -- it added a stricter, more ergonomic SYNTAX over the exact same prototype-and-delegation mechanics from section 2. Every method defined in a class body is installed on ClassName.prototype, non-enumerable by default (unlike object-literal methods, which are enumerable) -- a detail the sugar handles for you that manual Object.create chains do not.

class-vs-prototype-equivalence.jsjavascript
class Account {
  #balance; // private field -- truly inaccessible from outside, enforced by the engine, not convention
  static #minBalance = 0; // private static field, shared across all instances

  constructor(owner, openingBalance) {
    this.owner = owner;
    this.#balance = Math.max(openingBalance, Account.#minBalance);
  }

  deposit(amount) {
    this.#balance += amount;
    return this.#balance;
  }

  get balance() { return this.#balance; } // installed as an accessor on Account.prototype

  static open(owner) { return new Account(owner, 0); } // installed on Account itself, not .prototype
}

// THE DESUGARED EQUIVALENT -- what the engine effectively builds:
function AccountManual(owner, openingBalance) {
  this.owner = owner;
  this._balance = Math.max(openingBalance, 0); // no true privacy without # -- convention only
}
AccountManual.prototype.deposit = function (amount) {
  this._balance += amount;
  return this._balance;
};
Object.defineProperty(AccountManual.prototype, 'balance', {
  get() { return this._balance; },
  enumerable: false // class methods/accessors are non-enumerable BY DEFAULT -- literals are not
});
AccountManual.open = function (owner) { return new AccountManual(owner, 0); };

const acc = new Account('Priya', 100);
console.log(acc.deposit(50)); // 150
// console.log(acc.#balance); // SyntaxError outside the class body -- enforced at parse time, not just convention

Module 2 Recap

You now know that properties are governed by descriptors, not bare key-value pairs; that inheritance is live delegation through the [[Prototype]] chain rather than copied blueprints; that this is resolved by one of four precedence-ordered rules at the call-site (except for arrow functions, which use lexical scope resolution instead); and that class, extends, and #private fields are strict, ergonomic syntax compiling down to the exact same prototype mechanics. Module 3 shifts from objects to control flow: the Call Stack, the Event Loop, and how Promises and async/await actually schedule work.