Delegation vs classical inheritance
Think of JavaScript prototypes as runtime links between objects, compare them fairly with class-based inheritance, and choose delegation, duck typing, or composition for flexible designs.
- 01Compare object modelsExplain class-based inheritance and JavaScript’s prototype delegation without mixing the metaphors.
- 02Trace delegationPredict which object handles a property or method lookup, including OLOO examples.
- 03Design flexible behaviorUse duck typing and composition when a deep inheritance tree would hurt more than help.
Prototype links, not copies
The summary for this lesson says it plainly: think of prototypes as links between peers, not as copies. When an object does not have a property, JavaScript follows the object’s internal prototype link and asks the next object whether it has the property. Nothing is copied onto the receiver during that lookup.
Earlier in this module, The prototype chain showed the lookup walk, F.prototype & constructors showed how new links instances to a prototype object, Built-in prototypes showed where array and function methods live, and Object.create & null-prototype objects showed how to choose an object’s link directly. This last lesson puts the ideas together as a design tool.
Behavior delegation means an object handles what it owns and delegates missing property lookups to linked objects at runtime.
In many class-first languages, people picture a family tree of blueprints: a child blueprint extends a parent blueprint, then many finished objects are built from those plans. It is a clear story for stable is-a relationships.
- In real life: A parent blueprint
- In JavaScript: A base class or constructor prototype
- In real life: A child blueprint that specializes the plan
- In JavaScript: A subclass or derived constructor
- In real life: A finished house built from a plan
- In JavaScript: An instance object with its own data
Where the analogy stops: JavaScript can be written with class syntax, but the runtime still uses object links for shared methods. The blueprint story is useful only if you remember the underlying links.
JavaScript’s prototype model is closer to asking a colleague. If an object cannot answer a request, it asks the object it is linked to. The answer is used, but the method is not copied onto the first object.
- In real life: You cannot answer a question
- In JavaScript: The object does not have an own property
- In real life: You ask the colleague linked to your desk
- In JavaScript: Lookup follows
[[Prototype]] - In real life: The colleague answers without becoming you
- In JavaScript: The method stays on the linked object
Where the analogy stops: A real colleague chooses to help. JavaScript lookup is automatic and follows one prototype link at a time until it finds a property or reaches null.
Classical vs prototype-based OOP
STEP THROUGHObject-oriented programming groups data with behavior. Class-based OOP usually starts with categories: Widget, then a more specific Button. JavaScript supports that style, especially with class, but the language’s original sharing mechanism is prototype-based: objects linked to other objects.
The experiment below builds the same feature three ways. A Widget has render(). A Button adds clickLabel(). Switch between constructor/prototype inheritance, class extends, and OLOO. The output is identical; the links are what differ.
Switch the implementation, predict the same output, then step through where each method is found.
script
init(name) { this.name = name; }, render() { return "<button>" + this.name + "</button>"; },}; const Button = Object.create(Widget);Button.setup = function (name, label) { this.init(name); this.label = label;};Button.clickLabel = function () { return this.label + " clicked";}; const button = Object.create(Button);button.setup("Save", "Save");console.log(button.render());console.log(button.clickLabel());| Pattern | How behavior is shared | Strength | Watch out |
|---|---|---|---|
| Classical style | Constructor functions or class extends create instances linked to prototype objects | Familiar for is-a relationships and works with instanceof | Can encourage deep hierarchies when behavior really varies by feature |
| Prototype delegation / OLOO | Objects are linked directly with Object.create | Matches JavaScript’s lookup model with little ceremony | Less familiar to teams that expect class syntax |
| Composition | Small behavior objects or functions are combined into one object | Great for independent abilities and flexible combinations | Name clashes and this assumptions need discipline |
Classes come next module in Class basics, followed by Mixins & composition and OOP principles in JavaScript. This lesson does not teach class syntax in depth; it makes the prototype model honest before the class sugar arrives.
Behavior delegation
INTERACTIVEA delegated method is found somewhere along the prototype chain, but it still runs for the original receiver. In task.describe(), lookup may find describe on TaskActions, yet this inside that method is still task. That is why shared methods can read each object’s own data.
const Formatter = { formatStatus() { return this.done ? "done" : "todo"; },}; const TaskActions = Object.create(Formatter);TaskActions.describe = function () { return this.title + " is " + this.formatStatus();};TaskActions.complete = function () { this.done = true;}; const task = Object.create(TaskActions);task.title = "Write tests";task.done = false; console.log(task.describe());task.complete();console.log(task.describe());- task not here
- TaskActions handles it
Write tests is todoWrite tests is doneLooking for describe checks task → TaskActions. TaskActions handles it.
Trace it mechanically: start with the receiver before the dot; check own properties; move to the prototype; repeat. If a property is found, JavaScript uses it. If the chain reaches null, the lookup result is undefined. Calling that undefined as a function would then throw a TypeError.
If task uses TaskActions.describe, the method still lives on TaskActions. Add an own describe property to task and it shadows the delegated one; it does not edit the shared object.
OLOO: objects linked to other objects
Kyle Simpson’s You Don’t Know JS popularized the name OLOO: Objects Linked to Other Objects. The point is not that classes are evil. The point is that JavaScript’s native model is already about objects linked to other objects, so you can sometimes model behavior directly with plain objects.
const Widget = { init(name) { this.name = name; }, render() { return "<button>" + this.name + "</button>"; },}; const Button = Object.create(Widget);Button.setup = function (name, label) { this.init(name); this.label = label;};Button.clickLabel = function () { return this.label + " clicked";}; const button = Object.create(Button);button.setup("Save", "Save");console.log(button.render());console.log(button.clickLabel());OLOO comes from Kyle Simpson’s You Don’t Know JS books. It is a teaching name for a pattern JavaScript already supports with Object.create.
Notice the method syntax. The shared Widget methods are inline methods on an object literal, and Button receives its behavior as properties of a linked object. That keeps this calls safe under production minification and keeps the code close to the runtime model.
Duck typing: check the ability
INTERACTIVEDuck typing is the old phrase: if it walks like a duck and quacks like a duck, treat it as a duck. In code, that means checking for the capability you plan to use instead of checking the family name. If your function needs quack(), check for a callable quack. If it needs to loop over values, check for Symbol.iterator.
function speakLikeDuck(candidate) { if (typeof candidate?.quack === "function") { return candidate.quack(); } return "not a duck-like object";} function collectValues(candidate) { if (typeof candidate?.[Symbol.iterator] === "function") { return [...candidate].join(", "); } return "not iterable";} const robotDuck = { quack() { return "beep-quack"; } };console.log(speakLikeDuck(robotDuck));console.log(collectValues(new Set(["waddle", "quack"])));beep-quacknot iterablenot instanceof DuckThe lookalike passes because it has a real quack method.
typeof, and Symbol.iterator; no arbitrary code is evaluated.This is also why many JavaScript APIs accept array-like or iterable values instead of only arrays. TypeScript’s structural typing is a related compile-time idea: a value fits when its shape has the needed members, not because it was born in the right class family.
Good duck typing checks the exact capability before using it. The lesson code uses optional chaining and typeof so a missing method returns a friendly fallback instead of crashing.
Composition over inheritance
INTERACTIVEInheritance asks, “What family is this object in?” Composition asks, “What parts does this object have?” When behaviors combine in many independent ways, composition usually stays flatter and easier to change.
Instead of inventing a FlyingSwimmingSpeakingThing ancestor, snap together the abilities needed for this one object. Another object can reuse only the parts it needs.
- In real life: A flight module
- In JavaScript:
canFlybehavior - In real life: Waterproof fins
- In JavaScript:
canSwimbehavior - In real life: A voice box
- In JavaScript:
canSpeakbehavior - In real life: One robot assembled for a mission
- In JavaScript: One object composed from selected abilities
Where the analogy stops: Real robot parts have weight and wiring constraints. JavaScript behavior objects are lighter, but method names can still clash and methods may expect certain data on this.
const canFly = { fly() { return this.name + " lifts off"; },};const canSwim = { swim() { return this.name + " makes waves"; },};const canSpeak = { speak() { return this.name + " says hello"; },}; function createCreature(name, abilities) { return Object.assign({ name }, ...abilities);} const otterBot = createCreature("OtterBot", [canSwim, canSpeak]);console.log(otterBot.swim());console.log(otterBot.speak());OtterBotmissingOtterBot makes wavesOtterBot says helloOtterBot currently has swim(), speak(). Those are real own methods copied from the selected ability objects.
This is the “gorilla holding the banana” problem: you wanted one banana, but inheritance can make you take the gorilla holding it and the jungle behind the gorilla. Composition lets you pass around the banana-sized behavior.
Object.assign copies own properties. If two ability objects both define start, the later one overwrites the earlier one. And if a mixed-in method uses this, call it as a method of the composed object or bind it intentionally.
Where you’ll use this
These ideas show up constantly, even when you are not writing an OOP framework. Built-in arrays delegate methods to Array.prototype. UI objects often share behavior through prototypes or class methods. Plugin systems let a specific plugin delegate missing settings to a defaults object. Utility functions often use duck typing so they can accept arrays, Sets, Maps, strings, or any custom iterable.
Sort these scenarios by the design that fits best. There is no moral winner; the goal is to match the relationship in the problem.
- A
Buttonis a specializedWidgetand should share most widget behavior. - A plugin object should ask a shared defaults object for missing behavior at runtime.
- Some creatures can fly, some swim, some speak, and the combinations change often.
- A function only needs something with a
.quack()method. - Several objects should share a method but keep their own data as
this. - A custom error should be recognized as an
Errorand as its specific type.
Sort each scenario by the relationship it suggests: a stable is-a family, a runtime object link, or independent parts.
Common misconceptions
“A prototype method is copied into every object.”
It is shared by lookup. Each object keeps its own data; the method stays where it was defined unless you create an own property that shadows it.
“The class keyword means JavaScript stopped using prototypes.”
Class syntax is a nicer way to create constructor functions and prototype methods. The next module teaches the syntax; the lookup model remains prototype-based.
“Delegated methods run with this set to the prototype.”
In obj.method(), this is the original receiver, obj, even if method was found higher in the chain.
“Duck typing means never validating inputs.”
It means validating the capability you actually need, like a callable quack or an iterable protocol method.
“Composition automatically solves design.”
Composition avoids many hierarchy problems, but you still need clear names, conflict rules, and tests for the combinations you build.
Practice: delegation design
5 EXERCISESPredict the exact console output before you run the program.
const Shared = {
describe() {
return this.name + " via " + this.kind;
},
};
const item = Object.create(Shared);
item.name = "Panel";
item.kind = "delegation";
console.log(item.describe());item delegates describe to Shared. The call still uses item as this, so the output is Panel via delegation.
Rewrite the class hierarchy as objects linked to objects.
class Tool {
constructor(name) { this.name = name; }
label() { return "Tool: " + this.name; }
}
class Hammer extends Tool {
swing() { return this.name + " swings"; }
}const Tool = {
init(name) { this.name = name; },
label() { return "Tool: " + this.name; },
};
const Hammer = Object.create(Tool);
Hammer.setup = function (name) { this.init(name); };
Hammer.swing = function () { return this.name + " swings"; };
const hammer = Object.create(Hammer);
hammer.setup("Mini");The shared object is Tool; the specialized object is Hammer; a concrete hammer delegates through Hammer to Tool.
Write a function that accepts any value with a real quack() method.
function acceptsQuacker(value) {
return typeof value?.quack === "function" ? value.quack() : "no quack";
}
console.log(acceptsQuacker({ quack() { return "robot quack"; } }));function acceptsQuacker(value) {
return typeof value?.quack === "function" ? value.quack() : "no quack";
}
console.log(acceptsQuacker({ quack() { return "robot quack"; } }));The object did not come from a Duck constructor, but it has the capability the function needs: a callable quack.
Compose one object from canFly and canSwim, then call both methods.
const canFly = { fly() { return this.name + " flies"; } };
const canSwim = { swim() { return this.name + " swims"; } };
const duck = Object.assign({ name: "Ducky" }, canFly, canSwim);
console.log(duck.fly());
console.log(duck.swim());const canFly = { fly() { return this.name + " flies"; } };
const canSwim = { swim() { return this.name + " swims"; } };
const duck = Object.assign({ name: "Ducky" }, canFly, canSwim);
console.log(duck.fly());
console.log(duck.swim());The duck has only the selected ability methods. No FlyingSwimmingAnimal ancestor is needed.
A game starts with Animal, then adds FlyingAnimal, SwimmingAnimal, and FlyingSwimmingSpeakingAnimal. What pattern would avoid that deep tree?
Use composition. Deep names like FlyingSwimmingSpeakingThing reveal that the hierarchy is modeling combinations of abilities, not a stable family tree.
Quiz: check your understanding
7 QUESTIONSRead the code questions by following lookup exactly. Read the design questions by asking what relationship the scenario really has.
Question 1 of 7What is the key difference between classical inheritance and JavaScript prototype delegation?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictconst shared = { role: "shared" }; const user = Object.create(shared); user.name = "Ada"; console.log(user.role);Choose an answer to see the explanation.
Question 3 of 7In an inherited method call like
button.render(), what isthisinsiderender?Choose an answer to see the explanation.
Question 4 of 7What does OLOO stand for in this lesson?
Choose an answer to see the explanation.
Question 5 of 7What does this duck-typed check print?
Read the code, then predictfunction useIt(value) { return typeof value?.quack === "function" ? value.quack() : "skip"; } console.log(useIt({ quack() { return "beep"; } }));Choose an answer to see the explanation.
Question 6 of 7Which design is composition over inheritance?
Choose an answer to see the explanation.
Question 7 of 7What is a common pitfall when mixing behavior objects with
Object.assign?Choose an answer to see the explanation.
Key takeaways
- JavaScript prototypes are runtime links, not copied method bundles.
- Class syntax is useful, but shared class methods still live on prototypes.
- OLOO makes object-to-object links explicit with
Object.create. - Duck typing checks capabilities, such as
quack()orSymbol.iterator. - Composition builds flexible objects from small behavior parts and avoids deep combination hierarchies.
Remember the one-liner.
Delegation means: if this object cannot answer, ask the linked object; composition means: build the object from the behaviors it needs.
Up next: Class basics. You’ll learn the syntax, now with the prototype model already in your head.