Реализуйте поведение в стиле Array в JavaScript без использования Array

Есть ли способ создать похожий на массив объект в 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

Особое поведение, на которое я смотрю, заключается в том, что длина изменяется без вызова каких-либо методов. Из этого вопроса я понимаю, что оператор [] перегружен в случае массивов, и это объясняет это поведение. Есть ли способ, позволяющий воспроизвести это поведение чисто на языке javascript, или язык недостаточно гибок для этого?

Согласно документации Mozilla , значения, возвращаемые регулярным выражением, также делают странные вещи с этим индексом. Это возможно с простым javascript?

14.12.2008 01:19:37
9 ОТВЕТОВ
РЕШЕНИЕ

Теперь у нас есть ECMAScript 2015 (ECMA-262 6th Edition; ES6), у нас есть прокси-объекты , и они позволяют нам реализовать 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свойство инвариант), но он дает приблизительное представление о том, как его реализовать. Это также не имитирует поведение [[Call]] и [[Construct]] при включении Array, что является еще одной вещью, которую вы не могли сделать до ES6 - было невозможно иметь расхождение между двумя в коде ES, хотя ничего из этого не сложно.

Это реализует lengthсвойство так же, как спецификация определяет его как работающее: оно перехватывает присваивания свойствам объекта и изменяет lengthсвойство, если оно является «индексом массива».

В отличие от того, что можно сделать с ES5 и геттерами, это позволяет получать lengthв постоянное время (очевидно, это все еще зависит от того, что базовый доступ к свойствам в виртуальной машине является постоянным временем), и единственный случай, когда он обеспечивает непостоянную производительность по времени это не реализованный случай, когда newLen - oldLenсвойства удаляются (и удаление происходит медленно на большинстве виртуальных машин!).

7
16.01.2020 02:43:44
Одно замечание: isArrayIndex выдает ошибки, когда параметром pявляется Symbol: «Невозможно преобразовать значение Symbol в число». В моем случае я добавил чек наtypeof p === 'symbol'
moron4hire 10.01.2018 04:30:33
Ты клевый, чтобы выпустить это под MIT?
Brett Zamir 14.01.2020 05:39:54
@BrettZamir Я в порядке, если моя оригинальная версия используется под лицензией MIT (© Сэм Снеддон, 2016 г.), но я не могу говорить за Филипа Дупановича, который с тех пор сделал немало больше, так что для текущей версии вы бы Также необходимо получить их разрешение.
gsnedders 15.01.2020 13:29:26

Конечно, вы можете реплицировать практически любую структуру данных в JavaScript, все основные строительные блоки есть. Однако то, что вы в итоге получите, будет медленнее и менее интуитивно понятным.

Но почему бы просто не использовать push / pop?

-2
14.12.2008 01:24:59
это больше теоретический вопрос о границах JavaScript, чем что-либо практическое = P. Я думаю, что мой ответ заключается в том, что вы не можете сделать это.
Claudiu 14.12.2008 01:36:39

В большинстве случаев вам не нужен предопределенный размер индекса для массивов в 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]
1
14.12.2008 01:38:43
Кстати, чтобы ответить на ваш конкретный вопрос больше; Пользовательский объект, который лучше отслеживает индексы массива, если они важны для вашего дела, возможно, будет хранить для этого внутренний массив и методы получения / установки. Объект может заполнить пустые индексы значением 'null', чтобы лучше представить array.length.
finpingvin 14.12.2008 01:43:51

Оператор [] - это родной способ доступа к свойствам объекта. Он недоступен в языке для переопределения, чтобы изменить его поведение.

Если вам нужно вернуть вычисленные значения в операторе [], вы не можете сделать это в JavaScript, поскольку язык не поддерживает концепцию вычисляемого свойства. Единственное решение - использовать метод, который будет работать так же, как оператор [].

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

Если то, что вы хотите, имеет то же поведение, что и встроенный массив в вашем классе, всегда можно использовать встроенные методы массивов непосредственно в вашем классе. Внутренне ваш класс будет хранить данные так же, как и собственный массив, но будет сохранять свое состояние класса. 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);
}
23
14.12.2008 01:39:32
Начиная с JavaScript 1.8.5 вы можете иметь вычисляемые свойства. developer.mozilla.org/en-US/docs/JavaScript/Reference/…
mpen 12.02.2013 16:31:35
@mpen и ES5, для тех, кто заботится о спецификациях и о том, что делают другие браузеры (учитывая, что номера версий JS в основном просто кодируют версии SpiderMonkey!).
gsnedders 10.06.2016 22:46:09

Если вы беспокоитесь о производительности своего разреженного массива (хотя, вероятно, не должны этого делать) и хотите убедиться, что структура была такой же длины, как и элементы, которые вы ей передали, вы можете сделать это:

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

Опять же, стоит отметить, что вы вряд ли увидите прирост производительности при этом. Я подозреваю, что Javascript уже прекрасно обрабатывает разреженные массивы, большое спасибо.

1
14.12.2008 01:40:41
Я считаю, что ваш код на самом деле точно соответствует моему коду = P. [] является литералом для "new Array ()", а sup [0] и sup ['0'] ведут себя одинаково.
Claudiu 14.12.2008 01:59:53
извините, я не видел, чтобы у вас не был настроен начальный индекс.
Claudiu 14.12.2008 02:07:45
Массивы и Hashtables (и, следовательно, Objects) все тесно связаны в Javascript, и между ними нет большого различия. Массив со строковыми ключами ведет себя как Hashtable, поэтому в приведенном выше примере он не будет заполняться значениями от 1 до 4.
Jason Kester 16.12.2008 21:56:06

Да, вы можете легко разбить массив на массивоподобный объект в 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собственность. Который скорее выдувает все это.

Проблема более детально описана в разделе « Как создать подкласс JavaScript Array Object» Дина Эдвардса . Позже выяснилось, что его обходной путь не был безопасным, поскольку некоторые блокировщики всплывающих окон предотвратят это.

Обновление: Стоит упомянуть абсолютно эпический пост Юрия "kangax" Зайцева на эту тему. Это в значительной степени охватывает все аспекты этой проблемы.

13
4.03.2016 10:11:44
В комментариях к посту Дина есть несколько альтернативных решений, которые, кажется, работают довольно хорошо (за исключением некоторых проблем с производительностью).
Matt Kantor 14.12.2008 18:16:57
Это, в частности, выглядит интересно: webreflection.blogspot.com/2008/03/…
Matt Kantor 14.12.2008 18:17:33

Ответ таков: сейчас нет пути. Поведение массива определено в ECMA-262 как такое поведение и имеет явные алгоритмы для того, чтобы иметь дело с получением и установкой свойств массива (а не общих свойств объекта). Это меня несколько смущает = (.

2
14.12.2008 04:08:36
Начиная с ES2015 (или ES6, или как мы хотим это называть!), Мы можем делать это с помощью прокси.
gsnedders 10.06.2016 22:47:21
И я теперь отправил ответ, делающий такую ​​вещь!
gsnedders 10.06.2016 23:28:10

Это то, что вы ищете?

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

Единственный недостаток - длина будет повторяться в циклах for..in, как если бы это было свойство. Жаль, что нет способа установить атрибуты свойства (это одна вещь, которую я действительно хотел бы сделать).

3
14.12.2008 17:25:17
По общему признанию, это было бы чертовски медленно для больших случаев.
Matt Kantor 14.12.2008 18:23:05
да ... это тоже еще не стандарт, и это не то, как массивы реализованы в JS - но хороший ответ, это будет работать
Claudiu 14.12.2008 21:16:15
Object.defineProperty теперь позволяет делать перечислитель не перечисляемым в совместимых браузерах
OrangeDog 27.09.2011 16:45:29

Вы также можете создать свой собственный метод длины, например:

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