【问题标题】:Extend native JavaScript array扩展原生 JavaScript 数组
【发布时间】:2012-12-09 15:41:37
【问题描述】:

有没有办法从 JS 原生函数继承一个类?

例如,我有一个这样的 JS 函数:

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

我尝试将其转换为 Typescript,但失败了!!

export class Xarray implements Array {
}

编译器要求我定义所有Array 接口属性。我知道如果我需要这个Xarray.prototype = new Array();,我必须在 TS 中扩展Array

如何在TS中扩展JS原生对象?

【问题讨论】:

标签: javascript typescript


【解决方案1】:

从 TypeScript 1.6 开始,可以扩展 Array 类型,见What's new in TypeScript

这是一个例子:

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"

如果你要发射到 ES5 或更低版本,请使用以下代码:

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

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

阅读更多关于为什么这是必要的here

【讨论】:

  • 这对我来说在 typescript 2 中不起作用,请参阅:github.com/Microsoft/TypeScript/issues/13720 以及适用的东西:blog.simontest.net/extend-array-with-typescript-965cc1134b3
  • 从臀部射击,@SimonEpskamp,您的编译目标是 ES5。请参阅下面的答案,了解我为什么这么认为。尝试以 ES6 为目标,如果它开始工作,这个猜测得到证实。如果是这样,有关如何使其与 ES5 一起工作的详细信息在那个答案中。
  • @SimonEpskamp 感谢您指出这一点!不久前我遇到了与错误相同的问题。我更新了答案以包含针对 ES5 或更低版本的说明。
【解决方案2】:

我认为没有办法继承现有的接口,如 Array,

export class Xarray implements Array {

}

你应该创建一个函数并用它的原型继承它。 Typescript 也会接受它,类似于 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();

更新: 对此进行了很好的讨论,并在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);

希望对你有帮助!!!

【讨论】:

  • 如果我想在 YArray 的函数中使用 this.splice,我会得到一个编译错误。有什么建议可以解决这个问题吗?我似乎无法通过类型转换获得任何东西。
  • 哦,我现在明白了。为了能够使用您需要在虚拟类中实现它们的所有本机方法,而不仅仅是推送和弹出。
  • 因提供错误信息而被否决:“我认为没有办法继承现有的接口,如 Array”。有很多关于如何做到这一点的例子。所需要的只是声明你的方法。请参阅本页下方的示例。
  • 我必须将构造函数更改为 this.push.apply(this,items); return this; 才能使用初始元素(如 new XArray(1,2,3)
【解决方案3】:

是的,可以在 TS 中扩展本机 JS 对象,但是在扩展内置类型(lib.d.ts 中包含的那些)如 Array 时存在问题。阅读这篇文章以了解解决方法:http://typescript.codeplex.com/workitem/4

所以定义一个类型接口,在后期扩展原生类型对象可以通过以下方式完成:

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

使用一个具体的例子,你可以对数组中的一些元素进行排序,这些元素在接口中定义了一个排序函数,然后在一个对象上实现它。

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

【讨论】:

  • 您不能仅仅将 sort() 转储到那里,因为这目前还需要实现所有其他 Array 函数。此外,“数学”已经作为接口而不是类存在。
【解决方案4】:

在研究这个问题时,我在 Extending JavaScript Arrays While Keeping Native Bracket-Notation Functionality 上看到了 Ben Nadel 的精彩帖子。在对如何成功地将其转换为 TypeScript 产生了一些初步的困惑之后,我创建了一个可以被子类化的完全工作的 Collection 类。

它可以做 Array 可以做的所有事情,包括括号索引、用于循环构造(for、while、forEach)、映射等。

主要实现点是

  1. 在构造函数中创建一个数组,将方法添加到数组中并从构造函数返回
  2. 复制 Array 方法的虚拟声明以传递 implements Array

使用示例:

  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

我做了available as a gist,完成了测试和子类的示例实现,但我在这里提供了完整的源代码:

/*
 * 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;
}

当然,Identifiablefindremove 方法上的位是不需要的,但我仍然提供它们,因为一个完整的示例比没有的准系统集合更有用一点任何自己的方法。

【讨论】:

    【解决方案5】:

    返回对象的构造函数隐式地将this 的值替换为super() 的调用者。生成的构造函数代码必须捕获 super() 返回的任何内容并将其替换为 this

    内置类使用 ES6 new.target 进行修复,但 ES5 代码无法确保 new.target 具有调用构造函数的值。

    这就是为什么你的额外方法消失了 - 你的对象有错误的原型。

    你需要做的就是在调用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;
        }
      }
    }
    

    请注意,这个诅咒将折磨你的孩子和你孩子的孩子,直到代码结束;您必须在每一代继承链中进行修复。

    【讨论】:

      【解决方案6】:

      在您的情况下,一个不错的选择是使用这种模式:

      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
      }
      

      那么你可以这样做:

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

      但请注意:

      list.constructor  ; => Array and not XArray
      

      【讨论】:

        【解决方案7】:

        如果您已经有一个有效的 Xarray 实现,我认为在 typescript 中重新创建它没有意义,它最终会编译回 JavaScript。

        但我确实看到了能够在 TypeScript 中使用 Xarray 的意义。

        为了实现这一点,您只需为您的Xarray 提供一个接口。您甚至不需要具体的接口实现,因为您现有的 js 实现将作为一个实现。

        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 
        }
        

        完成此操作后,应该能够通过声明的变量使用您自定义的类型,而无需在 TypeScript 中实际实现它。

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

        您可以在此处查找参考资料,了解他们是如何键入 ECMAScript Array API 的: http://typescript.codeplex.com/SourceControl/changeset/view/2bee84410e02#bin/lib.d.ts

        【讨论】:

        • 你的意思是我不想创建 Xarray 实现 ts ?
        • 如果您已经在 J​​S 中实现了它,我认为在 TS 中重做它并冒着不再工作的风险是没有意义的。他们没有在 TypeScript 中重做整个 JS,而是在 lib.d.ts 中键入它,因此它可以通过 TS 使用。您还可以通过接口公开自定义类型并声明一个变量,通过该变量您可以使用 XArray 的 JS 实现。
        【解决方案8】:

        是的,您可以增加 Builtin 类型并以不需要其他答案中描述的 XArray 的所有用具的方式进行操作,并且更接近您在 javascript 中的操作方式。

        Typescript 允许使用多种方法来执行此操作,但对于像 Array 和 Number 这样的内置类型,您需要使用“合并”并声明全局命名空间来增加类型,请参阅 the docs

        所以我们可以为 Array 添加一个可选的元数据对象和一个 get first 成员

        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];
          }
        }
        

        我们可以这样使用:

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

        Number 也一样,说我想添加一个模函数,它不受余数 % 的限制

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

        我们可以这样使用它:

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

        对于我们可能想要添加 API 的函数,即使其表现得像一个函数和一个对象,我们可以使用 hybrid types (see 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;
        }
        

        像这样使用它:-

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

        【讨论】:

          【解决方案9】:

          为了克服原生 Array 类的扩展问题,我利用了装饰器。

          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
          }

          顺便说一句,如果使用装饰器工厂,这个装饰器可以变得更通用(对于其他本地类)。

          【讨论】:

            【解决方案10】:

            不知道对此有多不满,但例如我需要创建一个 BulletTypes 数组,以便我可以循环浏览它们。我所做的如下:

            interface BulletTypesArray extends Array<BulletType> {
                DefaultBullet?: Bullet; 
            }
            
            var BulletTypes: BulletTypesArray = [ GreenBullet, RedBullet ];
            BulletTypes.DefaultBullet = GreenBullet;
            

            显然你也可以创建一个通用接口,比如interface SuperArray&lt;T&gt; extends Array&lt;T&gt;

            【讨论】:

            • 他在问如何在TS中扩展JS原生对象。您正在做的是创建一个界面。这与他要求的实际实施细节无关。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-10-30
            • 2012-09-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多