【问题标题】:Implement Array-like behavior in JavaScript without using Array在 JavaScript 中实现类似数组的行为而不使用数组
【发布时间】: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吗?

【问题讨论】:

    标签: javascript arrays overloading


    【解决方案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);
    }
    

    【讨论】:

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

    【讨论】:

    【解决方案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];
      

      参考

      【讨论】:

        【解决方案10】:

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

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

        【讨论】:

        • 这更像是一个关于 javascript 限制的理论问题,而不是任何实际的问题 =P。我想我的回答是你不能这样做。
        猜你喜欢
        • 1970-01-01
        • 2010-10-26
        • 1970-01-01
        • 2017-10-21
        • 1970-01-01
        • 1970-01-01
        • 2019-04-14
        • 2021-12-05
        • 1970-01-01
        相关资源
        最近更新 更多