在不使用Array的情况下在JavaScript中实现类似于Array的行为

时间:2008-12-14 01:19:38

标签: javascript arrays overloading

有没有办法在JavaScript中创建类似数组的对象,而不使用内置数组?我特别关注这样的行为:

var sup = new Array(5);
//sup.length here is 0
sup[0] = 'z3ero';
//sup.length here is 1
sup[1] = 'o3ne';
//sup.length here is 2
sup[4] = 'f3our';        
//sup.length here is 5

我在这里看到的特殊行为是sup.length在没有调用任何方法的情况下发生变化。我从this question了解到[]运算符过载的情况下数组,这解释了这种行为。有没有一种纯粹的javascript方法来复制这种行为,或者语言不够灵活吗?

根据Mozilla docs,正则表达式返回的值也可以使用此索引执行时髦的操作。用普通的javascript可以实现吗?

10 个答案:

答案 0 :(得分:23)

[]运算符是访问对象属性的本机方式。它不能用语言覆盖以改变其行为。

如果您想要的是在[]运算符上返回计算值,则不能在JavaScript中执行此操作,因为该语言不支持计算属性的概念。唯一的解决方案是使用与[]运算符相同的方法。

MyClass.prototype.getItem = function(index)
{
    return {
        name: 'Item' + index,
        value: 2 * index
    };
}

如果您想要的内容与类中的本机Array具有相同的行为,则始终可以直接在您的类上使用本机Array方法。在内部,您的类将像本机数组一样存储数据但会保持其类状态。 jQuery这样做可以使jQuery类在保留其方法的同时具有数组行为。

MyClass.prototype.addItem = function(item)
{
    // Will add "item" in "this" as if it was a native array
    // it will then be accessible using the [] operator 
    Array.prototype.push.call(this, item);
}

答案 1 :(得分:13)

是的,您可以在JavaScript中轻松地将数组子类化为arraylike对象:

var ArrayLike = function() {};
ArrayLike.prototype = [];
ArrayLike.prototype.shuffle = // ... and so on ...

然后,您可以实例化类似对象的新数组:

var cards = new Arraylike;
cards.push('ace of spades', 'two of spades', 'three of spades', ... 
cards.shuffle();

不幸的是,这在MSIE中不起作用。它不跟踪length属性。这相当于整个事情的压缩。

Dean Edwards'How To Subclass The JavaScript Array Object更详细的问题。后来证明他的解决方法并不安全,因为一些弹出阻滞剂会阻止它。

更新:值得一提的是Juriy“kangax”Zaytsev的absolutely epic post。它几乎涵盖了这个问题的每个方面。

答案 2 :(得分:6)

现在我们有ECMAScript 2015(ECMA-262第6版; ES6),我们有proxy objects,它们允许我们在语言本身中实现Array行为,类似于:

function FakeArray() {
  const target = {};

  Object.defineProperties(target, {
    "length": {
      value: 0,
      writable: true
    },
    [Symbol.iterator]: {
      // http://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype-@@iterator
      value: () => {
        let index = 0;

        return {
          next: () => ({
            done: index === target.length,
            value: target[index++]
          })
        };
      }
    }
  });

  const isArrayIndex = function(p) {
    /* an array index is a property such that
       ToString(ToUint32(p)) === p and ToUint(p) !== 2^32 - 1 */
    const uint = p >>> 0;
    const s = uint + "";
    return p === s && uint !== 0xffffffff;
  };

  const p = new Proxy(target, {
    set: function(target, property, value, receiver) {
      // http://www.ecma-international.org/ecma-262/6.0/index.html#sec-array-exotic-objects-defineownproperty-p-desc
      if (property === "length") {
        // http://www.ecma-international.org/ecma-262/6.0/index.html#sec-arraysetlength
        const newLen = value >>> 0;
        const numberLen = +value;
        if (newLen !== numberLen) {
          throw RangeError();
        }
        const oldLen = target.length;
        if (newLen >= oldLen) {
          target.length = newLen;
          return true;
        } else {
          // this case gets more complex, so it's left as an exercise to the reader
          return false; // should be changed when implemented!
        }
      } else if (isArrayIndex(property)) {
        const oldLenDesc = Object.getOwnPropertyDescriptor(target, "length");
        const oldLen = oldLenDesc.value;
        const index = property >>> 0;
        if (index > oldLen && oldLenDesc.writable === false) {
          return false;
        }
        target[property] = value;
        if (index > oldLen) {
          target.length = index + 1;
        }
        return true;
      } else {
        target.property = value;
        return true;
      }
    }
  });

  return p;
}

我无法保证这实际上是完全正确的,并且它不会处理你将长度改变为小于其先前值的情况(这种行为有点复杂才能正确;大致它会删除属性,以便length属性不变量保持不变),但它概述了如何实现它。它也没有模仿Array上[[Call]]和[[Construct]]的行为,这是ES6之前你无法做到的另一件事 - 它不可能在ES代码中两者之间存在不同的行为,尽管这些都不是很难。

这实现length属性的方式与规范将其定义为工作方式相同:它拦截对象属性的赋值,如果它是"数组则更改length属性索引"

与ES5和getter不同的是,这允许一个人在恒定时间内获得length(显然,这仍然取决于VM中的基础属性访问是恒定时间),并且唯一的情况是它提供非恒定时间性能的是在删除newLen - oldLen属性时没有实现的情况(并且在大多数VM中删除速度很慢!)。

答案 3 :(得分:3)

这是你要找的吗?

Thing = function() {};
Thing.prototype.__defineGetter__('length', function() {
    var count = 0;
    for(property in this) count++;
    return count - 1; // don't count 'length' itself!
});

instance = new Thing;
console.log(instance.length); // => 0
instance[0] = {};
console.log(instance.length); // => 1
instance[1] = {};
instance[2] = {};
console.log(instance.length); // => 3
instance[5] = {};
instance.property = {};
instance.property.property = {}; // this shouldn't count
console.log(instance.length); // => 5

唯一的缺点是'length'将在for..in循环中迭代,就像它是属性一样。太糟糕了,没有办法设置property attributes(这是我真的希望我能做的一件事)。

答案 4 :(得分:2)

答案是:现在没办法了。数组行为在ECMA-262中定义为以这种方式运行,并且具有如何处理数组属性(而非通用对象属性)的获取和设置的显式算法。这有点令我失望=(。

答案 5 :(得分:1)

大多数情况下,javascript中的数组不需要预定义的索引大小,您可以这样做:

var sup = []; //Shorthand for an empty array
//sup.length is 0
sup.push(1); //Adds an item to the array (You don't need to keep track of index-sizes)
//sup.length is 1
sup.push(2);
//sup.length is 2
sup.push(4);
//sup.length is 3
//sup is [1, 2, 4]

答案 6 :(得分:1)

如果您担心稀疏阵列的性能(尽管您可能不应该这样做)并且希望确保结构只与您传递的元素一样长,那么您可以这样做:

var sup = [];
sup['0'] = 'z3ero';
sup['1'] = 'o3ne';
sup['4'] = 'f3our';        
//sup now contains 3 entries

同样,值得注意的是,通过这样做,您不会看到任何性能提升。我怀疑Javascript已经很好地处理了稀疏数组,非常感谢你。

答案 7 :(得分:1)

你也可以创建自己的长度方法,如:

Array.prototype.mylength = function() {
    var result = 0;
    for (var i = 0; i < this.length; i++) {
        if (this[i] !== undefined) {
            result++;
        }
    }
    return result;
}

答案 8 :(得分:0)

接口和实现

这种情况是对原始数组包装的简单实现,可以替换数据结构并引用通用接口即可实现。

export type IComparer<T> = (a: T, b: T) => number;

export interface IListBase<T> {
  readonly Count: number;
  [index: number]: T;
  [Symbol.iterator](): IterableIterator<T>;
  Add(item: T): void;
  Insert(index: number, item: T): void;
  Remove(item: T): boolean;
  RemoveAt(index: number): void;
  Clear(): void;
  IndexOf(item: T): number;
  Sort(): void;
  Sort(compareFn: IComparer<T>): void;
  Reverse(): void;
}


export class ListBase<T> implements IListBase<T> {
  protected list: T[] = new Array();
  [index: number]: T;
  get Count(): number {
    return this.list.length;
  }
  [Symbol.iterator](): IterableIterator<T> {
    let index = 0;
    const next = (): IteratorResult<T> => {
      if (index < this.Count) {
        return {
          value: this[index++],
          done: false,
        };
      } else {
        return {
          value: undefined,
          done: true,
        };
      }
    };

    const iterator: IterableIterator<T> = {
      next,
      [Symbol.iterator]() {
        return iterator;
      },
    };

    return iterator;
  }
  constructor() {
    return new Proxy(this, {
      get: (target, propKey, receiver) => {
        if (typeof propKey === "string" && this.isSafeArrayIndex(propKey)) {
          return Reflect.get(this.list, propKey);
        }
        return Reflect.get(target, propKey, receiver);
      },
      set: (target, propKey, value, receiver) => {
        if (typeof propKey === "string" && this.isSafeArrayIndex(propKey)) {
          return Reflect.set(this.list, propKey, value);
        }
        return Reflect.set(target, propKey, value, receiver);
      },
    });
  }
  Reverse(): void {
    throw new Error("Method not implemented.");
  }
  Insert(index: number, item: T): void {
    this.list.splice(index, 0, item);
  }
  Add(item: T): void {
    this.list.push(item);
  }
  Remove(item: T): boolean {
    const index = this.IndexOf(item);
    if (index >= 0) {
      this.RemoveAt(index);
      return true;
    }
    return false;
  }
  RemoveAt(index: number): void {
    if (index >= this.Count) {
      throw new RangeError();
    }
    this.list.splice(index, 1);
  }
  Clear(): void {
    this.list = [];
  }
  IndexOf(item: T): number {
    return this.list.indexOf(item);
  }
  Sort(): void;
  Sort(compareFn: IComparer<T>): void;
  Sort(compareFn?: IComparer<T>) {
    if (typeof compareFn !== "undefined") {
      this.list.sort(compareFn);
    }
  }
  private isSafeArrayIndex(propKey: string): boolean {
    const uint = Number.parseInt(propKey, 10);
    const s = uint + "";
    return propKey === s && uint !== 0xffffffff && uint < this.Count;
  }
}

案例

const list = new List<string>(["b", "c", "d"]);
const item = list[0];

参考

答案 9 :(得分:-2)

当然,您可以在JavaScript中复制几乎所有数据结构,所有基本构建块都在那里。然而,你最终会变得更慢,更不直观。

但为什么不使用push / pop?