问题
I'd like to add some functions to the Array class (I'd rather not have them as functions external to the class since it would ideally be discoverable when typing .
following the object). This is what I have so far:
export class List<T> extends Array<T> {
constructor(items?: Array<T>) {
super(...items)
Object.setPrototypeOf(this, List.prototype);
}
get first(): T {
return this[0]
}
}
this runs fine:
const list = new List([1,2,3]);
console.log(list.first)
but if I try to run this:
const list = new List([1,2,3]);
console.log(list.map(x=>x*2))
I get the following error:
super(...items)
^
TypeError: Found non-callable @@iterator
Ideally I would get an object back that is equivalent to new List(this.map(x=>x*2))
How can I extend the Array class without having to rewrite all the methods of Array?
回答1:
I think the problem here is that your List
constructor does not expect the same arguments as the Array
constructor.
When built-in methods like map()
create a new array, they construct it using a constructor found in the static Symbol.species class property. This is, by default, the same as the class constructor itself... unless you override it. So List[Symbol.species]
is List
. And List.prototype.map()
will end up calling new List(...)
. I'm pretty sure these methods expect the constructor at [Symbol.species]
to take the same arguments as the Array constructor, namely one of these overloads:
new Array(element0, element1[, ...[, elementN]]); // variadic arguments, one per item in array
new Array(arrayLength); // one numeric argument specifying length
But your List
constructor expects to treat its first (and only) argument items
as an iterable (since it uses spread syntax on it in the call to super(...items)
. When list.map(x=>x*2)
executes it calls something like new List(3)
, and you get an error about 3
not being iterable.
So, what can you do to fix this? By far the easiest way is to make sure that your List
constructor is compatible with the ArrayConstructor type, by having it take the same argument types.
The next easiest thing to do is to override List[Symbol.species]
and return the Array
constructor:
static get [Symbol.species](): ArrayConstructor {
return Array;
}
But that would mean that list.map(x => x*2)
returns an Array
and not a List
.
Assuming you really need your List
constructor to take a single iterable argument instead of the same variadic-or-maybe-a-single-number arguments as Array
, and assuming that you need list.map()
to return a List
, you can override the List[Symbol.species]
property with something more complicated:
static get [Symbol.species](): ArrayConstructor {
return Object.assign(function (...items: any[]) {
return new List(new Array(...items))
}, List) as any;
}
That essentially causes native methods to call new List(new Array(x,y,z))
instead of new List(x,y,z)
.
Okay, hope that makes sense and gives you some direction. Good luck!
回答2:
There is no need to set the prototype. The error occurs because the constructor runs a second time when the map is called and the length of the array is passed as an argument, so when you try to spread the argument on the super call, it throws an error because a number is not iterable.
constructor(items?: Array<T>) {
console.log(`I've received `, items);
items = items || [];
super(...items);
console.log(`Now i'm this`, this); //
// Object.setPrototypeOf(this, List.prototype);
}
Why does it happen? No idea! I do not have enough points yet, otherwise I'd put this as a comment! :-)
If you change the constructor to use ... to gather the arguments nothing will blow up:
constructor(...items: Array<T>) { //...
来源:https://stackoverflow.com/questions/54522949/how-can-i-extend-the-array-class-and-keep-its-implementations