How can I extend the Array class and keep its implementations

江枫思渺然 提交于 2019-12-08 05:47:35

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!