Extending built-ins & instanceof
Subclass Array and Map safely, control species constructors, predict instanceof, and customize instanceof with Symbol.hasInstance.
- 01Extend built-ins deliberatelyCreate Array and Map subclasses without changing global prototypes.
- 02Control returned collection typesUse Symbol.species when map and filter should return plain arrays.
- 03Read instanceof preciselyExplain its prototype-chain search and when Symbol.hasInstance overrides it.
The idea: customize a standard tool without changing everyone’s
JavaScript’s built-ins are already powerful. Array knows about numeric indexes and length. Map stores any value as a key. Classes let you build a specialized version of those tools with extends instead of changing the global tool for every file on the page.
That distinction matters because an earlier lesson warned you not to modify built-in prototypes. Adding Array.prototype.sum changes every array in the program. Creating class NumberList extends Array changes only the lists you intentionally create.
Imagine a standard school uniform with your name badge sewn on. It is still the same uniform, but the badge belongs to you. Extending a built-in adds behavior to your objects without changing everyone else's.
- In real life: A standard school uniform
- In JavaScript: The built-in
ArrayorMapbehavior - In real life: Your name badge
- In JavaScript: Methods you add in a subclass
- In real life: Other uniforms stay unchanged
- In JavaScript: Other arrays and maps are untouched
Where the analogy stops: A real uniform can be altered. A subclass creates new objects; it does not edit the built-in prototype.
Extending a built-in means writing a class that inherits from a built-in constructor such as Array or Map. The result keeps the built-in’s internal behavior and adds your own methods or overrides.
This lesson covers four connected tools: extending Array and Map, Symbol.species, ordinary instanceof , and Symbol.hasInstance. Custom errors use the same inheritance idea, but Throwing & custom errors gets its own lesson later.
Extending Array & Map
INTERACTIVEAn Array subclass is still an array: numeric indexes, length, push, pop, iteration, and methods like map still work. You add names that make sense for your domain.
class TodoList extends Array { d = new Set(); add(t) { this.push(t); return this.length; } complete(t) { this.d.add(t); } open() { return this.filter(Boolean); }} const todos = new TodoList("a", "b");todos.add("c");todos.complete("b");const open = todos.open();const up = todos.map(String);console.log(todos.length);console.log(open instanceof TodoList);console.log(up.constructor.name);- tasks[plan, build]
- .length works2
- demo map classTodoList
- map instanceof TodoListtrue
Default species points back to the subclass, so filter and map make TodoList results.
The TodoList example has a custom add method and a complete method, but it still grows with push, shrinks with pop, and reports length. That makes a subclass feel natural when the object really is a list with a few extra rules.
Map subclasses are often calmer because a map’s surface is smaller. The example below changes one behavior: a missing key returns a default value instead of undefined. The important line is super(entries), which lets Map initialize its real internal storage before the subclass adds its own field.
Watch a Map subclass keep Map's storage while customizing one method.
script
class DefaultMap extends Map { constructor(fallback, entries) { super(entries); this.fallback = fallback; } get(key) { return super.has(key) ? super.get(key) : this.fallback; }} console.log(scores.get("Ada"));console.log(scores.get("Lin"));console.log(scores instanceof Map);Symbol.species: what should map and filter make?
STEP THROUGHHere is the subtle part. Some built-in methods create a new collection. todos.map(...) does not mutate todos; it makes another array-like result. For an Array subclass, JavaScript asks a well-known symbol: “Which constructor should I use for the new result?”
When a new uniform is made from yours, should it keep your uniform type? By default JavaScript uses the same subclass. Symbol.species can instead say, “Use the standard Array type.”
- In real life: A new uniform is needed
- In JavaScript:
maporfiltercreates a new collection - In real life: It gets your uniform type
- In JavaScript: Array subclass methods return the subclass by default
- In real life: A rule says use the standard type
- In JavaScript:
static get [Symbol.species]() { return Array; }
Where the analogy stops: Species affects methods that consult it. It does not change the original object.
Predict whether map returns TodoList or Array, then step through the real run.
script
class TodoList extends Array { d = new Set(); add(t) { this.push(t); return this.length; } complete(t) { this.d.add(t); } open() { return this.filter(Boolean); }} todos.add("c");todos.complete("b");const open = todos.open();const up = todos.map(String);console.log(todos.length);console.log(open instanceof TodoList);console.log(up.constructor.name);The default is real: map and filter on an array subclass return the subclass. If that would confuse callers, add static get [Symbol.species]() { return Array; } so derived arrays are plain arrays. That trade-off is about API design: do result arrays need your custom methods, or should they be ordinary data?
instanceof walks the prototype chain
STEP THROUGHvalue instanceof Constructor is not a type-name check. In the ordinary case it looks for Constructor.prototype somewhere on value’s prototype chain. If it finds that object, the answer is true; otherwise it is false.
Step through the prototype-chain search that powers instanceof.
script
class Collection {}class Stack extends Collection {} console.log(stack instanceof Stack);console.log(stack instanceof Collection);console.log(stack instanceof Object);console.log(3 instanceof Number);Object.setPrototypeOf(stack, Collection.prototype);console.log(stack instanceof Stack);console.log(stack instanceof Collection);That explains three common surprises. First, subclass instances also count as instances of their base class. Second, primitives like 3 are not instances of wrapper constructors like Number. Third, changing an object’s prototype chain can change future instanceof answers. You rarely call Object.setPrototypeOf in production because it can hurt performance, but it is a great microscope for this rule.
[] instanceof Array[] instanceof Object3 instanceof Numbernew Number(3) instanceof Numbernew Map() instanceof ArrayObject.create(null) instanceof Object
Sort each expression by its real boolean result.
Symbol.hasInstance: redefining instanceof
STEP THROUGHA class can intercept instanceof by defining a static method named with Symbol.hasInstance. JavaScript calls that method with the left-hand value and trusts the boolean it returns. This is metaprogramming, so use it sparingly and document the rule clearly.
See how Symbol.hasInstance turns instanceof into a duck-typing check.
script
static [Symbol.hasInstance](value) { return typeof value?.quack === "function"; }} const toy = { quack() { return "squeak"; } };const rock = { sound: "thud" };console.log(toy instanceof Duck);console.log(rock instanceof Duck);The Duck example is duck typing: “If it quacks like a duck, treat it like a duck.” It accepts any object with a callable quack method, even if that object was never constructed by Duck. That can be elegant for small capability checks, but surprising if readers expect instanceof to mean “built by this class.”
Where you’ll use this
Most application code does not need many built-in subclasses. Reach for them when the phrase “is a kind of array/map” stays true after you add behavior. Good examples include a fixed vocabulary list with helper methods, a DefaultMap for counters, or a domain list that prints itself nicely in debugging tools.
class CounterMap extends Map {
add(key, amount = 1) {
const next = (this.get(key) ?? 0) + amount;
this.set(key, next);
return next;
}
}
const views = new CounterMap();
views.add("/home");
views.add("/home", 2);
console.log(views.get("/home")); // 3Notice that this example does not need Symbol.species because Map.prototype.set mutates and returns the same map. Species matters most when methods create new collections.
Pitfalls & trade-offs
Subclassing is safer than mutating built-in prototypes, but it is not free. Constructors, species, cross-realm values, and reader expectations all matter.
| Built-in | Subclass experience | Why |
|---|---|---|
| Array | Useful but surprising | length, holes, and species make it powerful; constructors need care. |
| Map | Usually smooth | Internal Map storage is initialized by super(), and custom reads are common. |
| Set | Usually smooth | Like Map, it has clear collection behavior and few constructor traps. |
| Error | Common, but separate lesson | Custom errors are important enough for Throwing & custom errors. |
| Promise | Awkward | Chaining, species, and async timing make Promise subclasses hard to reason about. |
| Date | Awkward | Date stores time in internal slots; wrapper utilities are often clearer. |
- Array for a small domain list with helper methods
- Map with a default value for missing keys
- Set that adds domain-specific helpers
- Promise with many custom chain methods
- Date subclass for every business calendar rule
- RegExp subclass that rewrites matching semantics
Sort each design idea by whether subclassing is usually comfortable or awkward.
new Array(3) creates an array with length 3 and empty slots, not [3]. If an Array subclass constructor passes one number to super, it inherits that behavior.
class BadList extends Array {
constructor(items) {
super(items);
}
}
class GoodList extends Array {
constructor(...items) {
super();
this.push(...items);
}
}
console.log(new BadList(3).length);
console.log(new BadList(["a", "b"]).length);
console.log(new GoodList(3).length);Prefer rest parameters when you mean “these are the items”: constructor(...items) { super(); this.push(...items); }. If you mean “an initial capacity,” say so clearly and test the empty-slot behavior.
Common misconceptions
- “Subclassing Array changes all arrays.” No. Editing
Array.prototypechanges all arrays; subclassing creates a separate class. - “Array methods always return plain arrays.” Not for subclasses.
mapandfilterreturn the subclass by default unless species says otherwise. - “instanceof checks the class name.” It checks prototype-chain identity, not a string name.
- “instanceof is a perfect type check.” Cross-realm objects, changed prototypes, primitives, and
Symbol.hasInstancecan all surprise you. UseArray.isArraywhen you specifically need arrays. - “Symbol.hasInstance is just syntax decoration.” It fully controls the right-hand class’s
instanceofresult.
Practice exercises
5 EXERCISESsum() to an Array subclassWrite an Array subclass with a method that returns the total of its numbers.
class NumberList extends Array {
sum() {
return this.reduce((total, value) => total + value, 0);
}
}
const scores = new NumberList(2, 3, 5);
console.log(scores.sum());class NumberList extends Array {
sum() {
return this.reduce((total, value) => total + value, 0);
}
}
const scores = new NumberList(2, 3, 5);
console.log(scores.sum());NumberList inherits array storage and reduce. The sum method adds each item and returns 10 for 2, 3, and 5.
Predict the two console lines before running the subclass.
class Tags extends Array {}
const tags = new Tags("js", "css");
const upper = tags.map(tag => tag.toUpperCase());
console.log(upper instanceof Tags);
console.log(upper.constructor.name);class Tags extends Array {}
const tags = new Tags("js", "css");
const upper = tags.map(tag => tag.toUpperCase());
console.log(upper instanceof Tags);
console.log(upper.constructor.name);Because Tags has no species override, map creates another Tags instance. The logs are true and Tags.
Add the species override and predict the two booleans.
class Tags extends Array {
static get [Symbol.species]() { return Array; }
}
const tags = new Tags("js", "css");
const upper = tags.map(tag => tag.toUpperCase());
console.log(upper instanceof Tags);
console.log(upper instanceof Array);class Tags extends Array {
static get [Symbol.species]() { return Array; }
}
const tags = new Tags("js", "css");
const upper = tags.map(tag => tag.toUpperCase());
console.log(upper instanceof Tags);
console.log(upper instanceof Array);The species getter tells map to construct plain arrays. The result is not a Tags, but it is still an Array.
hasInstanceDefine a class whose instanceof check accepts duck-shaped values.
class Quacker {
static [Symbol.hasInstance](value) {
return typeof value?.quack === "function";
}
}
console.log({ quack() {} } instanceof Quacker);
console.log({ quack: true } instanceof Quacker);class Quacker {
static [Symbol.hasInstance](value) {
return typeof value?.quack === "function";
}
}
console.log({ quack() {} } instanceof Quacker);
console.log({ quack: true } instanceof Quacker);The custom hook accepts objects with a callable quack method and rejects objects where quack is just a boolean.
Fix the constructor so new GoodList(3) contains the number 3.
class GoodList extends Array {
constructor(...items) {
super();
this.push(...items);
}
}
console.log(new GoodList(3).length);
console.log(new GoodList(3)[0]);class GoodList extends Array {
constructor(...items) {
super();
this.push(...items);
}
}
console.log(new GoodList(3).length);
console.log(new GoodList(3)[0]);constructor(...items) starts empty, then push adds the actual items. new GoodList(3) contains the value 3, so its length is 1 and index 0 is 3.
Check your understanding
7 QUESTIONSQuestion 1 of 7Why subclass Array instead of adding
Array.prototype.sum?Choose an answer to see the explanation.
Question 2 of 7What does the Bag
mapexample print?Read the code, then predictclass Bag extends Array {} const bag = new Bag("a", "b"); const copy = bag.map(x => x); console.log(copy instanceof Bag);Choose an answer to see the explanation.
Question 3 of 7What does the Bag species override print?
Read the code, then predictclass Bag extends Array { static get [Symbol.species]() { return Array; } } const bag = new Bag("a", "b"); console.log(bag.filter(Boolean) instanceof Bag);Choose an answer to see the explanation.
Question 4 of 7How does ordinary
value instanceof Constructordecide?Choose an answer to see the explanation.
Question 5 of 7What does the base-class instanceof example print?
Read the code, then predictclass Base {} class Child extends Base {} const value = new Child(); console.log(value instanceof Base);Choose an answer to see the explanation.
Question 6 of 7What is
Symbol.hasInstancefor?Choose an answer to see the explanation.
Question 7 of 7What does the Duck hasInstance example print?
Read the code, then predictclass Duck { static [Symbol.hasInstance](x) { return typeof x?.quack === "function"; } } console.log({ quack() {} } instanceof Duck);Choose an answer to see the explanation.
Key takeaways
- Subclass built-ins when your object really is a specialized built-in, and avoid changing global prototypes.
- Array subclass methods such as
mapandfilterreturn the subclass by default. Symbol.specieslets a subclass choose the constructor used for derived results.- Ordinary
instanceofsearches the prototype chain forConstructor.prototype. Symbol.hasInstancelets a class replace that search with a custom boolean test.
One-line definition: Extending built-ins creates a specialized version of a standard JavaScript object, while instanceof answers membership by prototype chain unless a class defines a custom has-instance hook.
Up next: Mixins & composition.