Why does `class MyArray extends Array {}` produce something that behaves like a real array — a live `length`, index-based behaviour — when the older `function MyArray() { Array.call(this); }` pattern never did?
answer
- arrays are exotic, not ordinary objects
- who allocates decides what you get
- calling versus constructing the parent
- new.target chooses the prototype
- derived methods consult a species
basics
~20 sArrays are exotic objects only their own constructor can create. With extends, super() runs Array as a constructor and it allocates the exotic object, using new.target to give it the subclass prototype. Array.call(this) merely calls Array as a function and discards this.
solid answer
~40 sAn array is an exotic object: its `length` updates automatically because the engine gave it special internal behaviour at allocation, and no amount of copying methods reproduces that. In a class hierarchy the *parent* allocates, so `super()` invokes `Array` as a constructor and gets a genuine exotic array back; `new.target` is forwarded, so `Array` reads `new.target.prototype` and stamps the instance with `MyArray.prototype`. You end up with a real array that also has your methods. The old pattern could not work: `Array.call(this)` calls `Array` as a plain function, which ignores its `this` and returns a brand-new array that is thrown away, leaving the ordinary object untouched. Methods like `map` and `filter` then return `MyArray` instances too, because they allocate through the constructor's `Symbol.species`.
code
javascript · 6 linesclass MyArray extends Array {}
const a = new MyArray();
a.push(1, 2, 3);
console.log(a.length, Array.isArray(a), a instanceof MyArray); // 3 true true
console.log(a.filter((x) => x > 1) instanceof MyArray); // truego deeper
Know that modern class syntax lets you extend built-ins like Array and Map and get an object that really behaves like one, which the old function-based pattern could not do.
Explain the mechanism: super() constructs the parent, the parent allocates the exotic object, and forwarded new.target supplies the subclass prototype.
Show what still bites in production — the parent's argument contract, Symbol.species deciding what derived methods return, and downlevel compilation silently reverting the behaviour.
Argue the design call: extending a built-in makes your type indistinguishable from it at every boundary, so weigh that against composition, engine optimisation, and what invariants a derived slice can honestly claim.
## Exotic objects and why methods are not enough Some built-ins are not ordinary objects. An array has *exotic* internal behaviour: defining an index property may grow `length`, and lowering `length` deletes elements. That behaviour is installed when the object is created and cannot be attached afterwards. The same is true of `Map` and `Set` (which carry internal data slots), `Error` (stack capture), typed arrays and `Function`. This is precisely what the ES5 inheritance pattern could not reproduce: ```js function MyArray() { Array.call(this); } MyArray.prototype = Object.create(Array.prototype); const a = new MyArray(); a[0] = 'x'; a.length; // 0 — not an array, just an object with array methods ``` `Array.call(this)` calls `Array` **as a function**. `Array` ignores its `this` entirely and returns a new array, which nobody keeps. The object `new MyArray()` produced is an ordinary object; it merely inherits `Array.prototype`'s methods, most of which then behave oddly because the object has no exotic length. ## What `extends` changes With a class, the derived constructor allocates nothing. `super()` invokes the parent **as a constructor**, so `Array` runs its real allocation path and produces a genuine exotic array. Two details make the result belong to the subclass: 1. `super()` forwards `new.target` unchanged — it is `MyArray`, not `Array`. 2. Built-in constructors take the prototype for the new object from `new.target.prototype`, falling back to their own only when `new.target` is themselves. So the object is exotic (created by `Array`) *and* has `MyArray.prototype` on its chain. ```js class MyArray extends Array {} const a = new MyArray(); a.push(1, 2, 3); a.length; // 3 Array.isArray(a); // true a instanceof MyArray; // true ``` This is the concrete payoff of the parent-allocates design, and it is worth naming as such: the rule that forces `super()` before `this` exists so that subclassing exotic built-ins works at all. ## Derived methods return the subclass Array methods that produce a new array — `map`, `filter`, `slice`, `splice`, `concat`, `flat` — do not hardcode `Array`. They allocate through the constructor found on the receiver, consulting its `Symbol.species` property: ```js new MyArray(1, 2, 3).filter(Boolean) instanceof MyArray; // true ``` If you would rather get plain arrays back — often the safer choice, since a subclass instance may carry invariants a filtered slice does not satisfy — override the species: ```js class MyArray extends Array { static get [Symbol.species]() { return Array; } } new MyArray(1, 2, 3).filter(Boolean) instanceof MyArray; // false ``` ## The traps that remain **The single-numeric-argument rule still applies.** `new MyArray(3)` creates a *length-3 empty* array, exactly as `new Array(3)` does; it does not create `[3]`. Subclassing does not let you reinterpret the parent's argument contract, because the parent is the one allocating. **Not every built-in tolerates it equally.** Subclassing `Map` or `Set` works because their constructors accept `new.target`, but methods you add cannot reach their internal storage except through the public API. Some host and legacy constructors ignore `new.target` altogether and always return an object with their own prototype, which silently discards your subclass. **Downlevel compilation breaks it.** If build tooling transpiles classes to ES5 functions, the parent-allocates mechanism is gone and you are back to the broken pattern — the classic symptom being a subclass instance whose `length` never updates. Targeting an ES2015+ output removes the whole class of problem. ## When to actually do it Extending `Array` is a legitimate but rarely necessary move. The instance is exotic, so every optimisation an engine applies to plain arrays is now applied to an object with a longer prototype chain, and a subclass leaking out of a module hands callers something that passes `Array.isArray` yet has extra behaviour. Composition — an ordinary class holding an array field — is usually the better default, and it makes the ownership of invariants explicit. Reach for `extends Array` when you genuinely need the value to *be* an array everywhere it travels, and set `Symbol.species` deliberately when you do.
- How do you make map and filter on an Array subclass return plain arrays instead of subclass instances?Define `static get [Symbol.species]() { return Array; }` on the subclass. The array methods that allocate a new array read the species from the receiver's constructor rather than hardcoding `Array`, so overriding it redirects them. This is often the right call: a filtered subset rarely satisfies whatever invariants the subclass exists to enforce.
- Does new MyArray(5) on a subclass of Array give you an array containing the number 5?No — it gives a length-5 array with no elements, exactly like `new Array(5)`. The parent constructor allocates, so its argument contract, including the single-numeric-argument special case, applies unchanged. A subclass constructor can normalise the arguments before calling `super(...)` if it wants different behaviour.
- Why does the same subclass stop behaving like an array once the code is compiled down to ES5?Because downlevel output rewrites classes as functions and replaces `super()` with a plain call to the parent, which reverts to the pattern where the child allocates an ordinary object. The exotic allocation never happens, so `length` stops tracking and index assignment does nothing useful. Emitting ES2015 or later class syntax is the fix.
saying these in an interview costs you the question
- Says extends Array just copies the array methods over
- Claims Array.call(this) initialises the current object
- Thinks new.target inside Array is Array itself
- Assumes filter on a subclass returns a plain array
- Believes any built-in can be subclassed identically