Extending built-in classes
Array, Map, Set, Promise — the classes baked into the language — are ordinary classes as far as extends is concerned. You can subclass them, add your own methods, and get an object that behaves like the original plus whatever you piled on top.
That opens the door to a couple of behaviors that surprise people the first time they hit them. Both come down to a single question: when a built-in method needs to make a new object, which constructor does it reach for?
Subclassing Array
Say you want an array of prices that can report its cheapest entry. Extend Array and add the method:
// PriceList is an Array with one extra method (you could add more)
class PriceList extends Array {
cheapest() {
return Math.min(...this);
}
}
let prices = new PriceList(40, 12, 8, 25, 100);
alert(prices.cheapest()); // 8
let bigTickets = prices.filter(item => item >= 25);
alert(bigTickets); // 40, 25, 100
alert(bigTickets.cheapest()); // 25
prices is a real array — indexing, length, push, filter, all of it works — and it also answers cheapest(). Nothing shocking yet.
The interesting line is the last one. prices.filter(...) produced a brand new array, and that new array also has cheapest(). So filter didn’t hand back a plain Array; it handed back a PriceList.
│ new result = new prices.constructor()
▼
How does filter know to do that? It reads the object’s constructor property. For our array:
prices.constructor === PriceList
When filter, map, slice, and friends need somewhere to put their results, they don’t hardcode Array. They look up this.constructor and build the new collection from that. Since prices.constructor is PriceList, you keep your custom methods all the way down a chain of transformations:
prices
.filter(x => x >= 25)
.map(x => x * 2)
.cheapest(); // still works — every step returns a PriceList
Overriding the result type with Symbol.species
Sometimes the default is exactly what you don’t want. Maybe PriceList is expensive to construct, or its methods only make sense on the original data, and you’d rather have filter and map return plain arrays.
You control this with a static getter keyed by the well-known symbol Symbol.species. If a class defines it, built-in methods call that getter to decide which constructor to use for their results — instead of falling back to this.constructor.
Return Array from it, and the derived arrays become ordinary arrays:
class PriceList extends Array {
cheapest() {
return Math.min(...this);
}
// built-in methods will use this as the constructor for their results
static get [Symbol.species]() {
return Array;
}
}
let prices = new PriceList(40, 12, 8, 25, 100);
alert(prices.cheapest()); // 8
// filter now creates its result using prices.constructor[Symbol.species]
let bigTickets = prices.filter(item => item >= 25);
// bigTickets is a plain Array, not a PriceList
alert(bigTickets.cheapest()); // Error: bigTickets.cheapest is not a function
prices itself is still a PriceList — prices.cheapest() works fine. But filter consulted Symbol.species, got Array back, and built its result as a plain array. The custom method doesn’t ride along anymore.
│ constructor = prices.constructor[Symbol.species] → Array
▼
No static inheritance between built-ins
Built-in classes carry their own static methods — the ones you call on the class itself, not on an instance: Object.keys, Object.defineProperty, Array.isArray, Date.now, and so on.
Here’s a rule you already know from normal classes: when class B extends A, B inherits both kinds of members. Instance methods flow through the prototype chain, and static methods flow too, because B itself gets A as its prototype. That was covered in Static properties and methods.
Built-in classes break that second half. They do inherit instance methods from each other, but they do not inherit static methods.
Consider Array and Date. Both are described in the spec as having Object.prototype at the top of their instance prototype chains — that’s why every array and every date has toString, hasOwnProperty, and the rest of the Object.prototype methods. So far, so normal.
But at the class level there’s no link. Array is not set up so that Array.[[Prototype]] is Object. So there is no Array.keys() static method inherited from Object, and no Date.keys() either. The instance chains connect; the constructor chains don’t.
Here’s the picture for Date and Object:
keys
…
╳
parse
…
toString: function
hasOwnProperty: function
…
toString: function
getDate: function
…
Read the two columns separately. On the right, the prototype chain is fully connected: a Date instance points to Date.prototype, which points to Object.prototype. That’s why dates get toString and hasOwnProperty. On the left, the constructor chain is severed — Date has no [[Prototype]] arrow to Object, so Object’s static methods never reach Date.
This is the one place built-in classes diverge from what plain extends gives you. When you write class Date2 extends Object, Date2 would inherit Object’s statics. The native Date deliberately doesn’t.