Implement Array-like behavior in JavaScript without using Array

后端 未结 10 1222
暖寄归人
暖寄归人 2020-12-13 07:45

Is there any way to create an array-like object in JavaScript, without using the built-in array? I\'m specifically concerned with behavior like this:

var sup         


        
10条回答
  •  刺人心
    刺人心 (楼主)
    2020-12-13 07:52

    Now we have ECMAScript 2015 (ECMA-262 6th Edition; ES6), we have proxy objects, and they allow us to implement the Array behaviour in the language itself, something along the lines of:

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

    I can't guarantee this is actually totally correct, and it doesn't handle the case where you alter length to be smaller than its previous value (the behaviour there is a bit complex to get right; roughly it deletes properties so that the length property invariant holds), but it gives a rough outline of how you can implement it. It also doesn't mimic behaviour of [[Call]] and [[Construct]] on Array, which is another thing you couldn't do prior to ES6—it wasn't possible to have divergent behaviour between the two within ES code, though none of that is hard.

    This implements the length property in the same way the spec defines it as working: it intercepts assignments to properties on the object, and alters the length property if it is an "array index".

    Unlike what one can do with ES5 and getters, this allows one to get length in constant time (obviously, this still depends on the underlying property access in the VM being constant time), and the only case in which it provides non-constant time performance is the not implemented case when newLen - oldLen properties are deleted (and deletion is slow in most VMs!).

提交回复
热议问题