Objects, Properties and Prototypes

Objects bundle related data and behaviour under names you control. This lesson covers literals and methods, the properties machinery (getters, freezing), and the idea underneath everything: the prototype chain. It ends with a real security contraexample — prototype pollution.

What an object is

A value with named slots

Primitives (lesson 04) hold one thing. An object holds many, each with a name (a key) — and the bundle itself is a single value you can pass around:

const user = {
  firstName: "Ada",        // key: value — commas BETWEEN members, none required last
  role: "admin",
  isActive: true,
};
console.log(user.firstName, user["role"]); // Ada admin — dot for known keys,
                                           // brackets when the key is in a variable
user.email = "ada@calc.dev";   // add a property at any time
delete user.isActive;          // remove one
console.log(Object.keys(user)); // [ 'firstName', 'role', 'email' ]

If you get this instead: undefined where a value should be — you read a key that does not exist; objects return undefined silently instead of erroring. Check with Object.hasOwn(user, "email").

Methods and this

A property whose value is a function is a method. Inside a method, this refers to the object the method was called on:

const user = {
  firstName: "Ada",
  describe() {                     // shorthand for: describe: function () {…}
    return `${this.firstName} (${this.role})`;
  },
  role: "admin",
};
console.log(user.describe()); // Ada (admin) — this = user, because user.describe()

this is decided by the call site, and losing it is the classic bug — the full rules get their own diagram in lesson 12 (classes).

Controlling properties

Getters, setters and freeze

const account = {
  cents: 0,
  get euros() { return this.cents / 100; },        // read like a property…
  set euros(value) { this.cents = value * 100; },  // …but code runs underneath
};
account.euros = 12.5;
console.log(account.cents); // 1250

const settings = Object.freeze({ theme: "dark", editor: { font: "mono" } });
// settings.theme = "light"; // TypeError in strict mode (silently ignored otherwise)
settings.editor.font = "sans"; // FREEZE IS SHALLOW — nested objects stay editable
console.log(settings.editor.font); // sans

Object.freeze locks the top level only. For a fully locked structure you freeze recursively — or, more often, you simply follow the "never mutate" discipline.

Prototypes: the invisible backup

Every object has one

When you read a property, the engine looks on the object first; if it is not there, it walks up the prototype chain — the object's backup, and the backup's backup — until it finds the name or hits the end:

Diagram: a dog instance walks its prototype chain — Dog.prototype holds bark, Object.prototype holds toString, then null; lookups stop at the first match

A property lookup walks up the chain and stops at the first match — that is why every object "has" toString.

const greeter = {
  greet() { return `hello, ${this.name}`; },
};
const ada = Object.create(greeter); // ada's prototype IS greeter
ada.name = "Ada";
console.log(ada.greet());                  // hello, Ada — found one level up
console.log(Object.hasOwn(ada, "greet"));  // false — inherited
console.log(Object.hasOwn(ada, "name"));   // true — owned

Object.hasOwn(obj, key) asks level 1 only — use it whenever "does this object have it" and "does the chain have it" differ. The in operator and for...in walk the whole chain.

Prototype pollution — a real security contraexample

One line breaks every object

const victim = {};
victim.__proto__.isAdmin = true; // writes into Object.prototype — EVERYONE'S backup!
console.log(({}).isAdmin);       // true — a brand-new object already "has" it

__proto__ is writable, so code that merges untrusted data into objects can poison the shared ancestor — and every object in the program inherits the poisoned keys. Real applications have been breached through exactly this. The defenses (run demo foundations/21_prototype_pollution.js): validate keys (__proto__, constructor), copy own keys only, use Map for untrusted dictionaries — and never write to __proto__.

Plain objects as records

Updates without mutation — and spread is shallow

In practice you will mostly build plain records and update them without mutation, using spread — which, note, is also shallow:

const updated = { ...user, role: "editor" }; // a NEW object; user untouched
console.log(updated.role, user.role);        // editor admin

Practice: model a record

The task

Run foundations/15_objects_lab.js with node, predicting each output. Then build a todo object with a done getter, freeze it, and try to break it — explain which level froze.

The checklist

  • You can add, read, and delete properties, and test ownership with Object.hasOwn.
  • You can explain what this is inside a method — and that the call site decides.
  • You can trace a property lookup through the prototype chain on the diagram.
  • You can explain prototype pollution in two sentences and name two defenses.