completefrontend
Open lab My progress
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 safelyMake your own Array and Map subclasses without changing the built-ins for everyone.
  • 02
    Choose what map and filter give backUse Symbol.species to make map and filter give back plain arrays.
  • 03
    Predict instanceofExplain how instanceof walks the prototype chain, and how Symbol.hasInstance can replace that walk.

The idea: your own version of a built-in

JavaScript comes with ready-made tools called built-ins. Array and Map are two of them. An Array keeps items in order and knows its length. A Map stores values under keys, and a key can be any value.

Sometimes you want one of these tools with a few extra methods. You can write your own class that starts from the built-in. You do it with extends, like class NumberList extends Array. Your new class is called a subclass. The built-in it starts from is called the base class.

An earlier lesson warned you not to change built-in prototypes. A prototype is the shared object where all arrays find their methods. If you add Array.prototype.sum, every array in the program gets sum, even arrays in other people's code. A subclass is different. Only the lists you make with new NumberList() get the new method.

Left: a plain array [2, 3, 5] holds a push() card. Right: new NumberList(2, 3, 5) holds a push() card and a sum() card. Only the NumberList has sum().[2, 3, 5]push()arrayno sum()new NumberList(2, 3, 5)push()sum()NumberListpush() + sum()
Both lists have push(), because both are arrays. Only the NumberList also has sum(). Plain arrays stay as they were.
Real-life analogyYour name badge on a school uniform

Every student wears the same school uniform. You sew your name badge onto yours. It is still the school uniform, but now it has your badge. The other students' uniforms don't get your badge.

In real life: The standard school uniform
In JavaScript: What every Array or Map can already do
In real life: Your name badge, sewn on
In JavaScript: The methods you add in your subclass
In real life: Other students' uniforms stay the same
In JavaScript: Other arrays and maps don't change
The main idea

Extending a built-in means writing a class that starts from a built-in, such as Array or Map. Your objects keep everything the built-in can do. You can add new methods, or replace an old method with your own version.

This lesson covers four tools that work together:

  • Extending Array and Map.
  • Symbol.species, which decides what kind of list methods like map give back.
  • instanceof, which asks "was this value made from this class?"
  • Symbol.hasInstance, which lets a class change how instanceof answers.

Custom errors use the same idea. They get their own lesson later: Throwing & custom errors.

Extending Array & Map

INTERACTIVE

A subclass of Array is still a real array. All of these still work:

  • Numbered places, like list[0].
  • length, which grows and shrinks by itself.
  • Methods like push, pop, and map.
  • Loops like for...of.

On top of that, you add methods with names that fit your program. Here is the same idea with a Map. A Map holds pairs: a key and its value.

A Map that can add points to a scorePop out in the code editor (opens in a new tab)JavaScript
class ScoreMap extends Map {
  add(name, points) {
    this.set(name, (this.get(name) ?? 0) + points);
  }
}
const scores = new ScoreMap();
scores.add("Asha", 3);
console.log(scores.get("Asha"));

Line 1 makes ScoreMap, a subclass of Map. Lines 2 to 4 add one new method, add. It does three things:

  • It reads the old score with this.get(name).
  • If there is no score yet, ?? 0 uses 0 instead. The ?? sign picks the right side when the left side is null or undefined.
  • It adds the new points and saves the total with this.set.

Line 7 gives Asha 3 points, so line 8 prints 3.

Try a TodoList that is still an array
A TodoList that is still an arrayPop 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]
  • length2
  • map gives back aTodoList
  • up instanceof TodoListtrue
Try it yourself

There is no species getter. So filter and map make new TodoList lists.

The push and pop buttons run real array methods on a small live list. The species button adds or removes the species getter in the code.

Try the playground. The TodoList class has its own add and complete methods. It still grows with push, shrinks with pop, and knows its length, because it is still an array. A subclass makes sense when your object really is a list that needs a few extra methods.

Map subclasses are often simpler, because a map has fewer methods than an array. The next example changes one thing. When a key is missing, get gives back a default value instead of undefined.

Look at line 3, super(entries). Calling super(...) runs the base class's constructor, here the Map constructor. It sets up the real map storage and adds the starting entries. Only after that can the subclass save its own fallback value on line 4. On line 7, super.has and super.get call Map's own versions of those methods.

Watch a DefaultMap give back a default value
Step 0 of 4Ready
Your turn: follow the blue line

Watch a Map subclass keep the map storage and change just one method.

Running in
  1. script
Next: line 11
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);
CallStoreChangeResultBlue line: runs next. 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 kind of list do map and filter make?

STEP THROUGH

Some array methods make a new list. todos.map(...) doesn't change todos. It builds a second list and gives it back. For a subclass, JavaScript has to choose: should the new list be your subclass, or a plain array?

JavaScript decides with a special property called Symbol.species. A symbol is a unique value that can be used as a property name. Symbol.species is one that JavaScript itself looks for.

map and filter both give back a TagsPop 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());
const short = tags.filter((tag) => tag.length === 2);
console.log(upper instanceof Tags);
console.log(short instanceof Tags);

Line 1 makes Tags, an empty subclass of Array. Line 2 makes a Tags list with "js" and "css". Line 3 makes a new list of the tags in capital letters. Line 4 makes a new list of only the tags that are 2 letters long. Lines 5 and 6 both print true. By default, the new lists are Tags too.

Real-life analogyMaking a new uniform from yours

A tailor copies your uniform. By default, the copy comes out in your style, badge and all. If you leave a note saying "use the standard uniform", the copy comes out plain. Symbol.species is that note.

In real life: The tailor makes a new uniform from yours
In JavaScript: map or filter makes a new list
In real life: It comes out in your style, badge and all
In JavaScript: By default, the new list is your subclass
In real life: A note says: use the standard uniform
In JavaScript: static get [Symbol.species]() { return Array; }
Step through map, filter, and species
Step 0 of 6Ready
Your turn: follow the blue line

Predict whether map gives back a TodoList or an Array. Then step through the real run.

Running in
  1. script
Next: line 15
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);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the second line of the class

Changing the choice starts a new run. Predict the last line 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.

To get plain arrays back, add this line inside the class: static get [Symbol.species]() { return Array; }. Here is what each part means:

  • static puts it on the class itself, not on each list.
  • get makes it a getter: a method that runs when someone reads the property.
  • [Symbol.species] is the property's name.
  • It returns Array, so new lists come out as plain arrays.
The tags list, a Tags holding js and css, runs tags.map(). With no species getter, the new list is a Tags. When species returns Array, the new list is a plain Array."js""css"tags.map()a new Tagsno species gettera plain Arrayspecies returns Array
map() always makes a new list. Symbol.species decides what kind of list it is.

Which one should you pick? Ask whether the new lists need your extra methods. If they do, keep the default. If they should be plain data, return Array.

Symbol.species doesn't matter for methods that change the list itself, like push or sort. Those change the original list. They never make a new one.

instanceof follows the prototype chain

STEP THROUGH

value instanceof Thing asks: "Was value made from Thing?" It gives back true or false. It doesn't check names. It follows a chain of objects instead.

Every object has a link to another object, called its prototype. That object has its own link, and so on. This line of links is the prototype chain. instanceof walks along the chain and looks for Thing.prototype. If it finds it, the answer is true. If the chain ends first, the answer is false.

A chain of four cards: stack, then Stack.prototype, then Collection.prototype, then Object.prototype. The check stack instanceof Collection walks down the chain and finds Collection.prototype, so the answer is true.stackStack.prototypeCollection.prototypeObject.prototypestack instanceof Collectionfound it: true
Each arrow points to the next prototype. instanceof walks down the chain and stops when it finds Collection.prototype.
Follow the instanceof walk along the chain
Step 0 of 7Ready
Your turn: follow the blue line

Step through the walk along the prototype chain that instanceof does.

Running in
  1. script
Next: line 4
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);
CallStoreChangeResultBlue line: runs next. 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.

This rule explains three surprises:

  • An object made by a subclass also counts as its base class. stack is a Stack, and it is also a Collection.
  • A primitive is a simple value like 3 or "hi". It is not an object, so it has no chain. That is why 3 instanceof Number is false.
  • If an object's chain changes, later answers change too.

Object.setPrototypeOf changes an object's prototype link. Real code rarely uses it, because it can make programs slower. Here it is useful, because it shows the rule clearly.

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

Put each line under the result it really gives.

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

Symbol.hasInstance: a class sets its own instanceof rule

STEP THROUGH

A class can decide for itself how instanceof answers. It adds a static method named Symbol.hasInstance. When you write value instanceof Duck, JavaScript calls Duck[Symbol.hasInstance](value). Whatever that method returns, true or false, becomes the answer.

This is powerful, and it can surprise people. Use it rarely, and write the rule down in a comment.

Let a class set its own instanceof rule
Step 0 of 4Ready
Your turn: follow the blue line

See how Symbol.hasInstance turns instanceof into a check of what an object can do.

Running in
  1. script
Next: line 1
  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);
CallStoreChangeResultBlue line: runs next. 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.
Left: toy holds a quack() function, so toy instanceof Duck is true. Right: rock holds sound: thud and no quack(), so rock instanceof Duck is false.toy instanceof Duckquack()toytrue: has quack()rock instanceof Ducksound: "thud"rockfalse: no quack()
Duck sets its own rule for instanceof: "Does it have a quack() function?" The toy does, so it passes. The rock does not.

This Duck example uses an idea called duck typing: "If it quacks like a duck, treat it like a duck." It checks what an object can do, not how it was made. Any object with a quack function passes, even though Duck never made it.

That is handy for small checks. But a reader may expect instanceof to mean "made by this class", so it can confuse them.

Where you’ll use this

Most programs need only a few built-in subclasses. Here is a good test. After you add your methods, is it still true to say "this is a kind of array" or "this is a kind of map"? If yes, a subclass fits. Some good examples:

  • A fixed list of words, with helper methods.
  • A DefaultMap for counting things.
  • A list for your app that is easy to read when you debug it.
A map that counts page viewsPop 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

Line 3 reads the old count, or 0 if there is none, and adds amount. Line 4 saves the new count. Lines 10 and 11 add 1 and then 2, so line 12 prints 3.

This example doesn't need Symbol.species. Map's set method changes the same map and gives back that same map. It never makes a new one. Symbol.species matters most for methods that make new lists.

You can also extend Error, the built-in for errors. A subclass can give the error a clear name. It can also keep the original problem as its cause.

An error that keeps the original problemPop out in the code editor (opens in a new tab)JavaScript
class CheckoutError extends Error {
  constructor(message, options) {
    super(message, options);
    this.name = "CheckoutError";
  }
}

const cause = new Error("payment declined");
const error = new CheckoutError("Could not place order", { cause });
console.log(error.name);
console.log(error.message);
console.log(error.cause.message);

Line 3, super(message, options), lets Error save the message and the cause. Line 4 sets name, the label that logs show. Line 8 makes the original error. Line 9 wraps it in a CheckoutError. Lines 10 to 12 print:

  • CheckoutError
  • Could not place order
  • payment declined

Keeping the cause helps later. The error tells the user what failed, and the cause tells you why it failed.

A static member belongs to the class itself, not to the objects it makes. So a subclass has two separate chains:

  • The class's own chain: NamedMap links to Map. This is how static methods are shared.
  • The objects' chain: NamedMap.prototype links to Map.prototype. This is how normal methods are shared.

So NamedMap instanceof Map is false, because Map.prototype is not on the class's own chain. But NamedMap.prototype instanceof Map is true.

The class and its objects have separate chainsPop out in the code editor (opens in a new tab)JavaScript
class NamedMap extends Map {}

console.log(NamedMap instanceof Map);
console.log(NamedMap.prototype instanceof Map);

Sometimes you only need a list that you control. Then composition can be simpler. Composition means your object holds an ordinary array, instead of being one. Other code can only use the methods you write, not every Array method.

A class that holds an array instead of being onePop out in the code editor (opens in a new tab)JavaScript
class TodoBag {
  constructor(items = []) {
    this.items = [...items];
  }
  add(item) {
    this.items.push(item);
  }
  count() {
    return this.items.length;
  }
}

const todos = new TodoBag(["plan"]);
todos.add("ship");
console.log(todos.count());
console.log(todos instanceof Array);

Line 3 copies the starting items into this.items. Line 13 makes the list with "plan", and line 14 adds "ship". Line 15 prints 2. Line 16 prints false, because todos is not an array.

Choose composition when your object is not really an array. Also choose it when fewer methods make the object easier for a team to use.

Pitfalls & trade-offs

A subclass is safer than changing a built-in prototype. But it still has a few traps. This section covers three:

  • Arrays that come from another realm, such as an iframe.
  • Built-ins that are awkward to extend.
  • The Array constructor when it gets a single number.

A realm is a separate JavaScript world with its own built-ins. Each iframe on a page is its own realm. (An iframe is a page shown inside another page.) Each realm has its own Array. So an array made in an iframe can give false for instanceof Array in the main page.

To check for an array, use Array.isArray. It works for arrays from any realm.

An array from another realm (Node demo)JavaScript
const vm = require("node:vm");
const otherRealmArray = vm.runInNewContext("[1, 2, 3]");
console.log(otherRealmArray instanceof Array);
console.log(Array.isArray(otherRealmArray));

Line 2 uses Node's vm module to make an array in a new realm. Line 3 prints false, and line 4 prints true. In a browser, the other realm would be an iframe instead.

The table below lists some built-ins. Each row says how it feels to extend that built-in, and why.

How easy is it to extend each built-in?
Built-inHow does it feel to extend?Why?
ArrayUseful, with a few surprises.It has length, empty places, and species. Its constructor needs care.
MapUsually smooth.super() sets up the map storage. Changing how get reads is a common, simple change.
SetUsually smooth.Like Map, it does one clear job and has few constructor traps.
ErrorCommon, with its own lesson.Custom errors get a full lesson later: Throwing & custom errors.
PromiseAwkward.Chaining, species, and waiting for results make Promise subclasses hard to follow.
DateAwkward.Date keeps its time in hidden internal storage. An object that holds a Date is often clearer.
Should you extend this built-in?
  • An Array for a small list with helper methods
  • A Map that gives a default value for missing keys
  • A Set with helper methods for your app
  • A Promise with many custom chain methods
  • A Date subclass for every business calendar rule
  • A RegExp subclass that changes how matching works
Try it yourself
0 of 6 correct

Put each idea under the group it fits: usually works well, or often awkward.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
The Array constructor and a single number

new Array(3) does not make [3]. It makes an array whose length is 3, with three empty places. If your subclass passes one number on to super, it gets the same behavior.

A constructor that breaks on one number, and a fixPop 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);

Line 14 prints 3, because a single number means length. Line 15 prints 1, because an array argument is just one item. Line 16 also prints 1, but this time the list really holds the number 3 at index 0.

When you mean "these are the items", use rest parameters. Writing ...items collects all the arguments into one array. Then start empty and push the items in: constructor(...items) { super(); this.push(...items); }.

If you really mean "make room for this many items", say so in a comment. Then test what happens with the empty places.

Common misconceptions

  • “Extending Array changes all arrays.” No. Changing Array.prototype changes all arrays. A subclass is a separate class, so only its own lists get the new methods.
  • “Array methods always give back plain arrays.” Not on a subclass. By default, map and filter give back your subclass, unless Symbol.species says otherwise.
  • “instanceof checks the class name.” No. It looks for one exact object, Thing.prototype, on the prototype chain. Names don't matter.
  • “instanceof always tells you the right type.” Not always. Values from another realm, changed prototypes, primitives, and Symbol.hasInstance can all surprise you. To check for an array, use Array.isArray.
  • “Symbol.hasInstance is just decoration.” No. It fully decides what instanceof answers for that class.

Practice exercises

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

Write a subclass of Array with a sum method. It should give back the total of all the numbers in the list.

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 what kind of list map gives back

    Read the code. Predict the two lines it prints, then run it.

    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 · PracticeMake map give back plain arrays

      This class has a species getter that returns Array. Predict the two true or false values it prints.

      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 your own instanceof rule

        Write a class whose instanceof check accepts any object that has a quack function.

        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) holds 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 make a subclass of 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 getter example 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 a normal value instanceof Constructor check 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

            • Extend a built-in when your object really is a kind of array or map. Don't change the built-in prototypes.
            • On a subclass of Array, map and filter give back the subclass by default.
            • Symbol.species lets a subclass choose what kind of list those methods make.
            • instanceof walks the prototype chain and looks for Thing.prototype.
            • Symbol.hasInstance lets a class replace that walk with its own true-or-false rule.

            One-line definition: Extending a built-in makes your own version of a standard JavaScript object, like an array or a map. instanceof checks whether a value was made from a class by walking the prototype chain, unless the class sets its own rule with Symbol.hasInstance.

            Up next: Mixins & composition.

            CompleteFrontend Clear concepts. Working examples.