Properties & Prototypes

The Course
•
4 min read

This course builds on JavaScript intermediate. You should already be comfortable with closures, modules, promises, async/await, AbortController, injected dependencies and basic assertions. Revisit async coordination, cancellation and integration tests if needed. No TypeScript is required.

We will design a small task-pool library. Its callers need a precise contract about concurrency, result order and failure. Before that, study language behavior that makes reusable objects and protocols work.

Property lookup first checks an object’s own properties, then follows its prototype chain. Object.create(base) makes a new object whose prototype is base. Assigning an ordinary writable property on the child usually creates an own property that shadows the inherited one; deleting that own property exposes the inherited value again. Object.hasOwn distinguishes ownership from lookup. Avoid modifying built-in prototypes: it changes shared behavior outside your library.

Before you start

This warm-up uses only intermediate material. Predict the result, then explain why awaiting the helper matters and why the original title keeps its spaces. If either step is unfamiliar, revisit async/await and immutable updates before continuing.

Prerequisite warm-up JavaScript
async function main() {
  const original = { title: " Notes " };

  async function clean(book) {
    return { ...book, title: book.title.trim() };
  }

  const result = await clean(original);

  if (result.title !== "Notes" || original.title !== " Notes ") {
    throw new Error("Copy contract failed");
  }

  console.log(result.title, result !== original);
}

main();
Check your prediction
Notes true

Predict, run, and explain

Write the output and trace the important state changes before checking it. Then run the program. Each editor is independent. Async examples end with main() so this editor waits for their work.

Predict, then run JavaScript
const base = { label: "shared" };
const child = Object.create(base);

console.log(child.label, Object.hasOwn(child, "label"));
child.label = "local";
console.log(child.label, base.label);
delete child.label;
console.log(child.label);
Check your prediction
shared false
local shared
shared

Trace lookup and ownership separately

A visible property need not be owned by the object you are inspecting. Lookup follows the prototype chain, while Object.hasOwn asks about the current object only. A default inherited from a prototype can therefore affect behavior without appearing among the object’s own keys.

Assignment to an ordinary writable inherited property creates a local shadow. Mutating an inherited object is different: you are following a shared reference and editing its contents. Draw the lookup chain and the reference graph separately before predicting a nested update.

Trace lookup and ownership separately: trace JavaScript
const base = { limit: 2 };
const child = Object.create(base);

console.log(child.limit);
console.log(Object.keys(child).length);
child.limit = 4;
console.log(Object.keys(child).join(","));
Check your prediction
2
0
limit

The first read finds the inherited default. Only after assignment does child own limit. Enumeration and property access answer different questions.

Trace lookup and ownership separately: contrast JavaScript
const base = { labels: ["shared"] };
const child = Object.create(base);

child.labels.push("changed");
console.log(base.labels.join(","));
child.labels = ["local"];
console.log(base.labels.join(","), child.labels.join(","));
Check your prediction
shared,changed
shared,changed local

Push changes the shared array found through lookup. Replacing child.labels later creates a local property but does not undo the mutation already made to the shared array.

Try the rule on new inputs

Test read, shadow, delete and nested mutation as separate operations. Explain why a library should not store mutable per-instance state in shared defaults, even when simple numeric defaults behave as expected.

Your turn

Exercise

Properties & Prototypes: practise

Create defaults with limit:2, then a settings object inheriting from defaults. Give settings its own limit:4. Print 4 2, then true for ownership. Delete the own setting and print 2.

Your workspace JavaScript
// Use Object.create and distinguish lookup from ownership.
Hint 1: the idea

An inherited property can be visible without belonging to the child.

Hint 2: a first step

Assign the override on settings, not on defaults.

Show one solution
const defaults = { limit: 2 };
const settings = Object.create(defaults);

settings.limit = 4;
console.log(settings.limit, defaults.limit);
console.log(Object.hasOwn(settings, "limit"));
delete settings.limit;
console.log(settings.limit);
4 2
true
2

Explain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?

Change it and test it

Add a nested array to defaults and push through settings. Explain why both references see the change: lookup did not copy the array. Then give each child its own array and verify independence. A library option snapshot should not accidentally mutate shared defaults.

Was this article helpful?

Your response is saved on this device.