cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Extending built-ins & instanceof

Subclass Array and Map safely, control species constructors, predict instanceof, and customize instanceof with Symbol.hasInstance.

By the end you can
  • 01
    Extend built-ins deliberatelyCreate Array and Map subclasses without changing global prototypes.
  • 02
    Control returned collection typesUse Symbol.species when map and filter should return plain arrays.
  • 03
    Read 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.

Real-life analogyYour name on a school uniform

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 Array or Map behavior
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.

The definition

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

INTERACTIVE

An 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.

Try a TodoList that is still an Array
TodoList subclassPop out in the code editor (opens in a new tab)JavaScript
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);
Live TodoList2 items
  • tasks[plan, build]
  • .length works2
  • demo map classTodoList
  • map instanceof TodoListtrue
Try it yourself

Default species points back to the subclass, so filter and map make TodoList results.

The buttons use real array operations on this tiny model. The code pane shows the teaching version used by the step-through replay.

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 DefaultMap return a fallback
Step 0 of 4Ready
Your turn: follow the blue line

Watch a Map subclass keep Map's storage while customizing one method.

Running in
  1. script
Next: line 11
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Symbol.species: what should map and filter make?

STEP THROUGH

Here 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?”

Real-life analogyThe school uniform rule

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: map or filter creates 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.

Step through map, filter, and species
Step 0 of 6Ready
Your turn: follow the blue line

Predict whether map returns TodoList or Array, then step through the real run.

Running in
  1. script
Next: line 15
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the line at the top of the class

Changing the line starts a fresh recording. Predict line 14 again.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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 THROUGH

value 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.

Follow the instanceof search
Step 0 of 7Ready
Your turn: follow the blue line

Step through the prototype-chain search that powers instanceof.

Running in
  1. script
Next: line 4
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

What does instanceof return?
  • [] instanceof Array
  • [] instanceof Object
  • 3 instanceof Number
  • new Number(3) instanceof Number
  • new Map() instanceof Array
  • Object.create(null) instanceof Object
Try it yourself
0 of 6 correct

Sort each expression by its real boolean result.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Symbol.hasInstance: redefining instanceof

STEP THROUGH

A 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.

Let a class define instanceof
Step 0 of 4Ready
Your turn: follow the blue line

See how Symbol.hasInstance turns instanceof into a duck-typing check.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  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);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

A practical counter mapPop out in the code editor (opens in a new tab)JavaScript
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")); // 3

Notice 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-ins that subclass well vs awkwardly
Built-inSubclass experienceWhy
ArrayUseful but surprisinglength, holes, and species make it powerful; constructors need care.
MapUsually smoothInternal Map storage is initialized by super(), and custom reads are common.
SetUsually smoothLike Map, it has clear collection behavior and few constructor traps.
ErrorCommon, but separate lessonCustom errors are important enough for Throwing & custom errors.
PromiseAwkwardChaining, species, and async timing make Promise subclasses hard to reason about.
DateAwkwardDate stores time in internal slots; wrapper utilities are often clearer.
Should you subclass this built-in?
  • 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
Try it yourself
0 of 6 correct

Sort each design idea by whether subclassing is usually comfortable or awkward.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Array constructor length trap

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.

The Array subclass constructor trapPop out in the code editor (opens in a new tab)JavaScript
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.prototype changes all arrays; subclassing creates a separate class.
  • “Array methods always return plain arrays.” Not for subclasses. map and filter return 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.hasInstance can all surprise you. Use Array.isArray when you specifically need arrays.
  • “Symbol.hasInstance is just syntax decoration.” It fully controls the right-hand class’s instanceof result.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upAdd sum() to an Array subclass

Write an Array subclass with a method that returns the total of its numbers.

Starter codePop out in the code editor (opens in a new tab)JavaScript
class NumberList extends Array {
  sum() {
    return this.reduce((total, value) => total + value, 0);
  }
}
const scores = new NumberList(2, 3, 5);
console.log(scores.sum());

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticePredict map’s return type

    Predict the two console lines before running the subclass.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    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);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeReturn plain arrays with species

      Add the species override and predict the two booleans.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      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);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeWrite a duck-typed hasInstance

        Define a class whose instanceof check accepts duck-shaped values.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        class Quacker {
          static [Symbol.hasInstance](value) {
            return typeof value?.quack === "function";
          }
        }
        console.log({ quack() {} } instanceof Quacker);
        console.log({ quack: true } instanceof Quacker);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeFix a broken Array subclass constructor

          Fix the constructor so new GoodList(3) contains the number 3.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          class GoodList extends Array {
            constructor(...items) {
              super();
              this.push(...items);
            }
          }
          console.log(new GoodList(3).length);
          console.log(new GoodList(3)[0]);

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Why subclass Array instead of adding Array.prototype.sum?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does the Bag map example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              class 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.

            3. Question 3 of 7What does the Bag species override print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              class 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.

            4. Question 4 of 7How does ordinary value instanceof Constructor decide?

              Choose an answer to see the explanation.

            5. Question 5 of 7What does the base-class instanceof example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              class Base {}
              class Child extends Base {}
              const value = new Child();
              console.log(value instanceof Base);

              Choose an answer to see the explanation.

            6. Question 6 of 7What is Symbol.hasInstance for?

              Choose an answer to see the explanation.

            7. Question 7 of 7What does the Duck hasInstance example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              class 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 map and filter return the subclass by default.
            • Symbol.species lets a subclass choose the constructor used for derived results.
            • Ordinary instanceof searches the prototype chain for Constructor.prototype.
            • Symbol.hasInstance lets 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.

            CompleteFrontend Clear concepts. Working examples.