web-dev-qa-db-fra.com

Étendre le tableau JavaScript natif

Est-il possible d'hériter une classe de la fonction native JS?

Par exemple, j'ai une fonction JS comme celle-ci:

function Xarray()
{
    Array.apply(this, arguments);
    //some stuff for insert, add and remove notification
}
Xarray.prototype = new Array();

J'ai essayé de le convertir en TypeScript mais j'ai échoué!

export class Xarray implements Array {
}

Le compilateur me demande de définir toutes les propriétés de l'interface Array. Je sais que si j'ai besoin de cette Xarray.prototype = new Array();, je dois étendre Array dans TS.

Comment étendre l'objet natif JS dans TS?

29
BalaKrishnan웃

Je ne pense pas qu'il y ait un moyen d'hériter des interfaces existantes comme Array,

export class Xarray implements Array {

}

Vous devez créer une fonction et en hériter avec son prototype. TypeScript l'acceptera également, ce qui est similaire à javascript.

function Xarray(...args: any[]): void; // required in TS 0.9.5
function Xarray()
{
    Array.apply(this, arguments);
   // some stuff for insert, add and remove notification
}
Xarray.prototype = new Array();

UPDATE: Celui-ci est bien discuté et constitue la meilleure solution pour cela à jqfaq.com .

//a dummy class it to inherite array.
class XArray {
    constructor() {
        Array.apply(this, arguments);   
        return new Array();
    }
    // we need this, or TS will show an error,
    //XArray["prototype"] = new Array(); will replace with native js arrray function
    pop(): any { return "" };
    Push(val): number { return 0; };
    length: number;
}
//Adding Arrray to XArray prototype chain.
XArray["prototype"] = new Array();

//our Class
class YArray extends XArray {
///Some stuff
}

var arr = new YArray();
//we can use the array prop here.
arr.Push("one");
arr.Push("two");

document.writeln("First Elemet in array : " + arr[0]);
document.writeln("</br>Array Lenght : " + arr.length);

J'espère que cela pourrait vous aider !!!

13
Rajagopal 웃

A partir de TypeScript 1.6, vous pouvez étendre le type Array, voir Quoi de neuf dans TypeScript

Voici un exemple:

class MyNewArray<T> extends Array<T> {
    getFirst() {
        return this[0];
    }
}

var myArray = new MyNewArray<string>();
myArray.Push("First Element");
console.log(myArray.getFirst()); // "First Element"

Si vous émettez vers ES5 ou une version antérieure, utilisez le code suivant:

class MyNewArray<T> extends Array<T> {
    constructor(...items: T[]) {
        super(...items);
        Object.setPrototypeOf(this, MyNewArray.prototype);
    }

    getFirst() {
        return this[0];
    }
}

En savoir plus sur la raison pour laquelle cela est nécessaire ici .

36
David Sherret

Oui, il est possible d’étendre un objet JS natif dans TS, mais il existe un problème qui étend les types intégrés (ceux inclus dans lib.d.ts) comme Array. Lisez ce post pour contourner le problème: http://TypeScript.codeplex.com/workitem/4

Donc, définir une interface de type qui étend un objet de type natif à un stade ultérieur peut être effectué de la manière suivante:

/// <reference path="lib.d.ts"/>
interface Array {
    sort: (input: Array) => Array;
}

En utilisant un exemple concret, vous pouvez trier certains éléments d’un tableau qui définissent une fonction de tri dans une interface et l’implémentent ultérieurement sur un objet.

class Math implements Array {
    sort : (x: Array) => Array {
          // sorting the array
    }
}
var x = new Math();
x.sort([2,3,32,3]);
8
Simo Endre

En faisant des recherches à ce sujet, je suis tombé sur l'excellent article de Ben Nadel sur Extension des tableaux JavaScript tout en conservant la fonctionnalité de notation de support native . Après une certaine confusion initiale sur la façon de convertir cela avec succès en TypeScript, j'ai créé une classe Collection entièrement opérationnelle pouvant être sous-classée.

Il peut faire tout ce qu’un tableau peut faire, y compris l’indexation par des crochets, son utilisation dans des constructions de boucles (pour, tout, pour chaque), des cartes, etc.

Les principaux points de mise en oeuvre sont

  1. Créez un tableau dans le constructeur, ajoutez les méthodes au tableau et renvoyez-le depuis le constructeur
  2. Copier les déclarations factices des méthodes Array pour transmettre le bit implements Array

Exemple d'utilisation:

  var foo = new Foo({id : 1})
  var c = new Collection();

  c.add(foo)
  c.length === 1;    // => true

  foo === c[0];      // => true
  foo === c.find(1); // => true

Je l'ai fait disponible en tant que Gist , avec des tests et un exemple d'implémentation d'une sous-classe, mais je présente le source complet ici:

/*
 * Utility "class" extending Array with lookup functions
 *
 * TypeScript conversion of Ben Nadel's Collection class.
 * https://Gist.github.com/fatso83/3773d4cb5f39128b3732
 *
 * @author Carl-Erik Kopseng
 * @author Ben Nadel (javascript original)
 */

export interface Identifiable {
    getId : () => any;
}

export class Collection<T extends Identifiable> implements Array<T> {

    constructor(...initialItems:any[]) {
        var collection = Object.create(Array.prototype);

        Collection.init(collection, initialItems, Collection.prototype);

        return collection;
    }

    static init(collection, initialItems:any[], prototype) {
        Object.getOwnPropertyNames(prototype)
            .forEach((prop) => {
                if (prop === 'constructor') return;

                Object.defineProperty(collection, prop, { value: prototype[prop] })
            });

        // If we don't redefine the property, the length property is suddenly enumerable!
        // Failing to do this, this would fail: Object.keys([]) === Object.keys(new Collection() )
        Object.defineProperty(collection, 'length', {
            value: collection.length,
            writable: true,
            enumerable: false
        });

        var itemsToPush = initialItems;
        if (Array.isArray(initialItems[0]) && initialItems.length === 1) {
            itemsToPush = initialItems[0];
        }
        Array.prototype.Push.apply(collection, itemsToPush);

        return collection;
    }

    // Find an element by checking each element's getId() method
    public find(id:any):T;

    // Find an element using a lookup function that
    // returns true when given the right element
    public find(lookupFn:(e:T) => boolean):T ;

    find(x:any) {
        var res, comparitor;

        if (typeof x === 'function') {
            comparitor = x;
        } else {
            comparitor = (e) => {
                return e.getId() === x;
            }
        }

        res = [].filter.call(this, comparitor);

        if (res.length) return res[0];
        else return null;
    }

    // Add an element
    add(value:T);

    // Adds all ements in the array (flattens it)
    add(arr:T[]);

    add(arr:Collection<T>);

    add(value) {

        // Check to see if the item is an array or a subtype thereof
        if (value instanceof Array) {

            // Add each sub-item using default Push() method.
            Array.prototype.Push.apply(this, value);

        } else {

            // Use the default Push() method.
            Array.prototype.Push.call(this, value);

        }

        // Return this object reference for method chaining.
        return this;

    }

    remove(elem:T):boolean;

    remove(lookupFn:(e:T) => boolean):boolean ;

    remove(x:any):boolean {
        return !!this._remove(x);
    }

    /**
     * @return the removed element if found, else null
     */
    _remove(x:any):T {
        var arr = this;
        var index = -1;

        if (typeof x === 'function') {

            for (var i = 0, len = arr.length; i < len; i++) {
                if (x(this[i])) {
                    index = i;
                    break;
                }
            }

        } else {
            index = arr.indexOf(x);
        }

        if (index === -1) {
            return null;
        }
        else {
            var res = arr.splice(index, 1);
            return res.length ? res[0] : null;
        }
    }


    // dummy declarations
    // "massaged" the Array interface definitions in lib.d.ts to fit here
    toString:()=> string;
    toLocaleString:()=> string;
    concat:<U extends T[]>(...items:U[])=> T[];
    join:(separator?:string)=> string;
    pop:()=> T;
    Push:(...items:T[])=> number;
    reverse:()=> T[];
    shift:()=> T;
    slice:(start?:number, end?:number)=> T[];
    sort:(compareFn?:(a:T, b:T) => number)=> T[];
    splice:(start?:number, deleteCount?:number, ...items:T[])=> T[];
    unshift:(...items:T[])=> number;
    indexOf:(searchElement:T, fromIndex?:number)=> number;
    lastIndexOf:(searchElement:T, fromIndex?:number)=> number;
    every:(callbackfn:(value:T, index:number, array:T[]) => boolean, thisArg?:any)=> boolean;
    some:(callbackfn:(value:T, index:number, array:T[]) => boolean, thisArg?:any)=> boolean;
    forEach:(callbackfn:(value:T, index:number, array:T[]) => void, thisArg?:any)=> void;
    map:<U>(callbackfn:(value:T, index:number, array:T[]) => U, thisArg?:any)=> U[];
    filter:(callbackfn:(value:T, index:number, array:T[]) => boolean, thisArg?:any)=> T[];
    reduce:<U>(callbackfn:(previousValue:U, currentValue:T, currentIndex:number, array:T[]) => U, initialValue:U)=> U;
    reduceRight:<U>(callbackfn:(previousValue:U, currentValue:T, currentIndex:number, array:T[]) => U, initialValue:U)=> U;
    length:number;
[n: number]: T;
}

Bien sûr, les bits sur les méthodes Identifiable, find et remove ne sont pas nécessaires, mais je les fournis quand même, car un exemple à part entière est un peu plus utilisable qu'une collection complète sans aucune méthode propre.

5
oligofren

Les constructeurs qui renvoient un objet substituent implicitement la valeur this aux appelants de super(). Le code constructeur généré doit capturer tout ce que super() renvoie et le remplacer par this.

Les classes intégrées utilisent ES6 new.target pour effectuer la correction, mais le code ES5 ne permet pas de s'assurer que new.target a une valeur appelant le constructeur.

C'est pourquoi vos méthodes supplémentaires disparaissent - votre objet a un prototype incorrect.

Tout ce que vous avez à faire est de réparer la chaîne de prototypes après avoir appelé super().

export class RoleSet extends Array {
  constructor() {
    super();
    Object.setPrototypeOf(this, RoleSet.prototype);
  }
  private containsRoleset(roleset:RoleSet){
      if (this.length < roleset.length) return false;
      for (var i = 0; i < roleset.length; i++) {
        if (this.indexOf(roleset[i]) === -1) return false;
      }
      return true;
  }
  public contains(item: string | RoleSet): boolean {
    if (item) {
      return typeof item === "string" ? 
        this.indexOf(item) !== -1 : 
        this.containsRoleset(item);
    } else {
      return true;
    }
  }
}

Sachez que cette malédiction affligera vos enfants et les enfants de vos enfants jusqu'à la fin du code; vous devez effectuer la réparation dans chaque génération d'une chaîne d'héritage.

3
Peter Wone

Dans votre cas, un bon pari serait d'utiliser ce modèle:

function XArray(array) {
  array = array || [];

  //add a new method
  array.second = function second() {
    return array[1];
  };

  //overwrite an existing method with a super type pattern
  var _Push = array.Push;
  array.Push = function Push() {
    _Push.apply(array, arguments);
    console.log("pushed: ", arguments);
  };

  //The important line.
  return array
}

Ensuite, vous pouvez faire:

var list = XArray([3, 4]);
list.second()   ; => 4

list[1] = 5;
list.second()   ; => 5

notez cependant que:

list.constructor  ; => Array and not XArray
1
Will Tomlins

Oui, vous pouvez augmenter les types Builtin et le faire d’une manière qui n’exige pas tout le matériel nécessaire à un tableau X, comme décrit dans les autres réponses, et qui est plus proche de la façon dont vous le feriez en JavaScript.

TypeScript permet cela de plusieurs façons, mais pour les types intégrés tels que Array et Number, vous devez utiliser "fusion" et déclarer l'espace de noms global pour augmenter les types, voir les docs

donc pour Array, nous pouvons ajouter un objet de métadonnées facultatif et obtenir un premier membre

declare global {
  interface Array<T> {
    meta?: any|null ,
    getFirst(): T
  }
}

if(!Array.prototype.meta )
{
  Array.prototype.meta = null
}
if(!Array.prototype.getFirst )
{
  Array.prototype.getFirst = function() {
    return this[0];
  }
}

nous pouvons l'utiliser comme ceci:

let myarray: number[] = [ 1,2,3 ]
myarray.meta = { desc: "first three ints" }
let first: number = myarray.getFirst()

Il en va de même pour Number dire que je veux ajouter une fonction modulo qui n’est pas limitée comme le reste%

declare global {
  interface Number {
    mod(n:number): number
  }
}

if(!Number.prototype.mod )
{
  Number.prototype.mod = function (n: number) {
          return ((this % n) + n) % n;
  }
}

et nous pouvons l'utiliser comme ceci:

let foo = 9;
console.log("-9.mod(5) is "+ foo.mod(5))    

Pour les fonctions auxquelles nous pouvons vouloir ajouter une API, c'est-à-dire pour la faire se comporter comme une fonction et un objet, nous pouvons utiliser types hybrides (voir docs)

// augment a (number) => string  function with an API
interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

//helper function to get my augmented counter function with preset values
function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

utilisez-le comme suit: -

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
1
Karl

Si vous avez déjà une implémentation Xarray en bon état de marche, je ne vois pas l'intérêt de la recréer dans TypeScript, qui sera finalement compilée en JavaScript.

Mais je vois l’intérêt de pouvoir utiliser la variable Xarray dans TypeScript. 

Pour ce faire, vous avez simplement besoin d’une interface pour votre Xarray. Vous n'avez même pas besoin d'une implémentation concrète de votre interface, car votre implémentation existante de js en servira une.

interface Xarray{
    apply(...arguments : any[]) : void;
    //some stuff for insert, add and ...
}
declare var Xarray: {
   new (...items: any[]): Xarray;
   (...items: any[]): Xarray;
   prototype: Array; // This should expose all the Array stuff from ECMAScript 
}

Cela fait, vous devriez pouvoir utiliser votre type défini personnalisé par le biais de la variable déclarée sans l'implémenter réellement dans TypeScript.

var xArr = new Xarray();
xArr.apply("blah", "hehe", "LOL");

Vous pouvez chercher une référence ici pour voir comment ils ont saisi le ECMAScript Array API: http://TypeScript.codeplex.com/SourceControl/changeset/view/2bee84410e02#bin/lib.d.ts

1
Jani Hyytiäinen

Afin de surmonter le problème de l'extension de la classe native Array, j'ai profité d'un décorateur.

function extendArray(constructor: Function) {
    Object.getOwnPropertyNames(constructor.prototype)
        .filter(name => name !== 'constructor')
.forEach(name => {
    const attributes = Object.getOwnPropertyDescriptor(constructor.prototype, name);
    Object.defineProperty(Array.prototype, name, attributes);
  });
}

@extendArray
export class Collection<T> extends Array<T> {
  constructor(...args: T[]) {
    super(...args);
  }
  // my appended methods
}

BTW Ce décorateur peut être plus générique (pour les autres classes natives) si vous utilisez une usine de décorateur.

0
Alexander Levakov