在 JavaScript 中实现类似数组的行为而不使用数组

Posted

技术标签:

【中文标题】在 JavaScript 中实现类似数组的行为而不使用数组【英文标题】:Implement Array-like behavior in JavaScript without using Array 【发布时间】:2010-09-26 19:59:26 【问题描述】:

有没有办法在 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吗?

【问题讨论】:

【参考方案1】:

[] 运算符是访问对象属性的本机方式。无法在语言中覆盖以更改其行为。

如果你想要在 [] 运算符上返回计算值,你不能在 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);

【讨论】:

从 JavaScript 1.8.5 开始,您可以拥有计算属性。 developer.mozilla.org/en-US/docs/JavaScript/Reference/… @mpen 和 ES5,对于那些关心规范和其他浏览器做什么的人(鉴于 JS 版本号基本上只是编码 SpiderMonkey 版本!)。【参考方案2】:

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

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 关于这个主题。它几乎涵盖了这个问题的方方面面。

【讨论】:

在 Dean 的帖子的 cmets 中有一些替代解决方法似乎效果很好(除了一些性能问题)。 这个看起来特别有趣:webreflection.blogspot.com/2008/03/…【参考方案3】:

现在我们有了 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 属性被删除时(并且在大多数虚拟机中删除速度很慢!),提供非恒定时间性能是未实现的情况。

【讨论】:

注意:当参数p 是符号时,isArrayIndex 错误:“无法将符号值转换为数字”。就我而言,我添加了typeof p === 'symbol' 的支票 @BrettZamir 我很好,我的原始版本在 MIT 许可下使用 (© 2016 Sam Sneddon),但我不能代表 Filip Dupanović 从那以后做了更多的事情,因此,对于当前版本,您还需要获得他们的许可。【参考方案4】:

这就是你要找的吗?

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(这是我真的希望我能做的一件事)。

【讨论】:

诚然,这对于大型实例来说会非常慢。 是的...这还不是一个标准,也不是在 JS 中实现数组的方式 - 但是很好的答案,这将起作用 Object.defineProperty 现在允许在兼容的浏览器中将 getter 设为不可枚举【参考方案5】:

答案是:目前还没有办法。数组行为在 ECMA-262 中被定义为这种行为方式,并且有明确的算法来处理 array 属性(而不是通用对象属性)的获取和设置。这让我有些沮丧 =(.

【讨论】:

从 ES2015(或 ES6 或任何我们想称呼它的名字)开始,我们可以使用代理来做到这一点。 我现在已经发布了一个答案!【参考方案6】:

大多数情况下,您不需要为 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]

【讨论】:

顺便说一句,更多地回答您的具体问题;一个自定义对象可以更好地跟踪数组索引,如果它们对您的事业很重要,也许会为此存储一个内部数组和 getter/setter 方法。该对象可以用“null”填充空索引以更好地表示array.length。【参考方案7】:

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

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

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

【讨论】:

我相信您的代码实际上完全等同于我的代码 =P。 [] 是“new Array()”的字面量,sup[0] 和 sup['0'] 的行为相同。 哦,抱歉,我没有看到您没有设置初始索引。 数组和哈希表(因此,对象)在 Javascript 中都是密切相关的,它们之间没有太大区别。带有字符串键的数组的行为类似于 Hashtable,因此在上面的示例中,它不会用 1 到 4 之间的值填充自己。【参考方案8】:

您还可以创建自己的长度方法,例如:

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

【讨论】:

【参考方案9】:

接口与实现

案例是对原数组封装的简单实现,可以替换数据结构,参考通用接口即可实现。

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

参考

proxy [Symbol.iterator]()

【讨论】:

【参考方案10】:

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

但是为什么不直接使用 push/pop 呢?

【讨论】:

这更像是一个关于 javascript 限制的理论问题,而不是任何实际的问题 =P。我想我的回答是你不能这样做。

以上是关于在 JavaScript 中实现类似数组的行为而不使用数组的主要内容,如果未能解决你的问题,请参考以下文章

swift 在swift中实现动态运行时对象,类似于javascript

javascript中实现类似php 的var_dump

在 Python 中实现类似列表的索引访问

在 SKNode 子类中实现 anchorPoint

如何在 iOS 中实现错误栏?

如何在 Spark 中实现“交叉连接”?