Почему объекты не являются итерабельными в JavaScript?

Почему объекты по умолчанию не повторяются?

Я все время вижу вопросы, связанные с итерационными объектами, причем общее решение заключается в переходе по свойствам объекта и доступе к значениям внутри объекта таким образом. Это кажется настолько распространенным явлением, что это заставляет меня задаться вопросом, почему сами объекты не являются итерируемыми.

Заявления, подобные ES6 for...of, было бы неплохо использовать для объектов по умолчанию. Поскольку эти функции доступны только для специальных "итерируемых объектов", которые не включают объекты {}, нам нужно пройти через обручи, чтобы сделать эту работу для объектов, для которых мы хотим их использовать.

Оператор for... создает цикл, итерации по итерационным объектам(включая массив, карту, набор, объект аргументов и т.д.)...

Например, используя функцию генератора ES6 :

var example = {a: {e: 'one', f: 'two'}, b: {g: 'three'}, c: {h: 'four', i: 'five'}};

function* entries(obj) {
   for (let key of Object.keys(obj)) {
     yield [key, obj[key]];
   }
}

for (let [key, value] of entries(example)) {
  console.log(key);
  console.log(value);
  for (let [key, value] of entries(value)) {
    console.log(key);
    console.log(value);
  }
}

Приведенное выше правильно регистрирует данные в том порядке, в котором я ожидаю, когда я запускаю код в Firefox (который поддерживает ES6):

output of hacky for...of

По умолчанию объекты {} не являются итерабельными, но почему? Разве недостатки перевешивают потенциальные выгоды от объектов, которые могут быть итерабельны? Какие проблемы связаны с этим?

Кроме того, поскольку объекты {} отличаются от коллекций "Array-like" и "итерируемых объектов", таких как NodeList, HtmlCollection и arguments, они не могут быть преобразованы в массивы.

Например:

var argumentsArray = Array.prototype.slice.call(arguments);

или использоваться с методами массива:

Array.prototype.forEach.call(nodeList, function (element) {}).

Помимо вопросов, которые у меня выше, мне бы хотелось увидеть рабочий пример о том, как сделать объекты {} в итерабельности, особенно от тех, кто упомянул [Symbol.iterator].. Это должно позволить эти новые {} "итерируемые объекты" используют такие выражения, как for...of. Кроме того, я задаюсь вопросом, можно ли сделать объекты итерабельными, чтобы они были преобразованы в массивы.

Я попробовал код ниже, но получаю TypeError: can't convert undefined to object.

var example = {a: {e: 'one', f: 'two'}, b: {g: 'three'}, c: {h: 'four', i: 'five'}};

// I want to be able to use "for...of" for the "example" object.
// I also want to be able to convert the "example" object into an Array.
example[Symbol.iterator] = function* (obj) {
   for (let key of Object.keys(obj)) {
     yield [key, obj[key]];
   }
};

for (let [key, value] of example) { console.log(value); } // error
console.log([...example]); // error

Ответ 1

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

1. Зачем добавлять конструкцию for...of в первую очередь?

JavaScript уже включает конструкцию for...in, которая может использоваться для итерации свойств объекта. Тем не менее, он не представляет собой цикл forEach, поскольку он перечисляет все свойства объекта и имеет тенденцию работать только в простых случаях.

Он разбивается в более сложных случаях (в том числе с массивами, где его использование обычно обескуражено или полностью запутано с помощью гарантий, необходимых для использования for...in с массивом правильно). Вы можете обойти это, используя hasOwnProperty (между прочим), но немного неуклюжий и неэлегантный.

Таким образом, мое предположение заключается в том, что конструкция for...of добавляется для устранения недостатков, связанных с конструкцией for...in, и обеспечивает большую полезность и гибкость при итерации. Люди склонны рассматривать for...in как цикл forEach, который обычно можно применять к любой коллекции и производить разумные результаты в любом возможном контексте, но это не то, что происходит. Цикл for...of фиксирует это.

Я также предполагаю, что важно, чтобы существующий код ES5 работал под ES6 и производил тот же результат, что и в ES5, поэтому нельзя нарушать изменения, например, поведением конструкции for...in.

2. Как работает for...of?

Справочная документация некоторых типов объектов, и относительно ясно, что использование for...of просто делегирует функции итератора.

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

Практические вопросы, не относящиеся к делу, возможно, было сочтено концептуально противоречивым, чтобы запустить все объекты с помощью нового заранее определенного свойства или косвенно сказать, что "каждый объект является коллекцией".

3. Почему объекты не iterable с помощью for...of по умолчанию?

Я предполагаю, что это комбинация:

  • По умолчанию все объекты iterable могут считаться неприемлемыми, поскольку он добавляет свойство, где ранее не было ни одного, или потому, что объект не является (обязательно) коллекцией. Как отмечает Феликс, "что значит перебирать функцию или объект регулярного выражения"?
  • Простые объекты уже можно повторить с помощью for...in, и неясно, какая реализация встроенного итератора могла бы быть выполнена по-другому/лучше, чем существующее поведение for...in. Так что даже если # 1 ошибочно, и добавление свойства было приемлемым, оно, возможно, не считалось полезным.
  • Пользователи, которые хотят создавать свои объекты iterable, могут легко сделать это, указав свойство Symbol.iterator.
  • Спецификация ES6 также предоставляет тип Map, который по умолчанию равен iterable и имеет некоторые другие небольшие преимущества перед использованием простой объект как Map.

Там даже пример приведен для № 3 в справочной документации:

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};

for (var value of myIterable) {
    console.log(value);
}

Учитывая, что объекты легко могут быть сделаны iterable, что они уже могут быть повторены с помощью for...in и что, вероятно, не будет ясно согласовано то, что должен делать итератор объекта по умолчанию (если то, что он делает, должно быть каким-то образом отличается от того, что делает for...in), кажется достаточно разумным, что объекты по умолчанию не были сделаны iterable.

Обратите внимание, что код вашего примера можно переписать с помощью for...in:

for (let levelOneKey in object) {
    console.log(levelOneKey);         //  "example"
    console.log(object[levelOneKey]); // {"random":"nest","another":"thing"}

    var levelTwoObj = object[levelOneKey];
    for (let levelTwoKey in levelTwoObj ) {
        console.log(levelTwoKey);   // "random"
        console.log(levelTwoObj[levelTwoKey]); // "nest"
    }
}

... или вы также можете сделать свой объект iterable так, как вы хотите, выполнив что-то вроде следующего (или вы можете сделать все объекты iterable вместо этого вместо Object.prototype[Symbol.iterator]):

obj = { 
    a: '1', 
    b: { something: 'else' }, 
    c: 4, 
    d: { nested: { nestedAgain: true }}
};

obj[Symbol.iterator] = function() {
    var keys = [];
    var ref = this;
    for (var key in this) {
        //note:  can do hasOwnProperty() here, etc.
        keys.push(key);
    }

    return {
        next: function() {
            if (this._keys && this._obj && this._index < this._keys.length) {
                var key = this._keys[this._index];
                this._index++;
                return { key: key, value: this._obj[key], done: false };
            } else {
                return { done: true };
            }
        },
        _index: 0,
        _keys: keys,
        _obj: ref
    };
};

Вы можете играть с этим здесь (в Chrome, в аренду): http://jsfiddle.net/rncr3ppz/5/

Edit

И в ответ на ваш обновленный вопрос, да, можно преобразовать iterable в массив, используя оператор распространения в ES6.

Однако, похоже, это пока не работает в Chrome, или, по крайней мере, я не могу заставить его работать в моем jsFiddle. Теоретически это должно быть так просто:

var array = [...myIterable];

Ответ 2

Я думаю, вопрос должен быть "почему нет встроенной итерации объектов?

Добавление итерации к самим объектам может иметь непреднамеренные последствия, и нет, нет способа гарантировать порядок, но писать итератор так же просто, как

function* iterate_object(o) {
    var keys = Object.keys(o);
    for (var i=0; i<keys.length; i++) {
        yield [keys[i], o[keys[i]]];
    }
}

Тогда

for (var [key, val] of iterate_object({a: 1, b: 2})) {
    console.log(key, val);
}

a 1
b 2

Ответ 3

Object не выполняют итерационные протоколы в Javascript по очень веским причинам. Существует два уровня, в которых свойства объекта могут быть повторены в JavaScript:

  • уровень программы
  • уровень данных

Итерация уровня программы

Когда вы перебираете объект на уровне программы, вы просматриваете часть структуры вашей программы. Это рефлексивная операция. Позвольте проиллюстрировать этот оператор с типом массива, который обычно повторяется на уровне данных:

const xs = [1,2,3];
xs.f = function f() {};

for (let i in xs) console.log(xs[i]); // logs `f` as well

Ответ 4

Вы можете легко сделать все объекты итерабельными глобально:

Object.defineProperty(Object.prototype, Symbol.iterator, {
    enumerable: false,
    value: function * (){
        for(let key in this){
            if(this.hasOwnProperty(key)){
                yield [key, this[key]];
            }
        }
    }
});

Ответ 5

Это новейший подход (который работает в хром канареечном)

var files = {
    '/root': {type: 'directory'},
    '/root/example.txt': {type: 'file'}
};

for (let [key, {type}] of Object.entries(files)) {
    console.log(type);
}

Да entries теперь является методом, который является частью Object:)

Изменить

После того, как вы посмотрели на него, вы можете сделать следующее

Object.prototype[Symbol.iterator] = function * () {
    for (const [key, value] of Object.entries(this)) {
        yield {key, value}; // or [key, value]
    }
};

чтобы теперь вы могли сделать это

for (const {key, value:{type}} of files) {
    console.log(key, type);
}

edit2

Вернемся к исходному примеру, если вы хотите использовать вышеупомянутый метод прототипа, он хотел бы как это сделать

for (const {key, value:item1} of example) {
    console.log(key);
    console.log(item1);
    for (const {key, value:item2} of item1) {
        console.log(key);
        console.log(item2);
    }
}

Ответ 6

Технически, это не ответ на вопрос, почему? но я адаптировал ответ Джека Слокума выше в свете комментариев BT к чему-то, что может быть использовано для того, чтобы сделать Object итеративным.

var iterableProperties={
    enumerable: false,
    value: function * () {
        for(let key in this) if(this.hasOwnProperty(key)) yield this[key];
    }
};

var fruit={
    'a': 'apple',
    'b': 'banana',
    'c': 'cherry'
};
Object.defineProperty(fruit,Symbol.iterator,iterableProperties);
for(let v of fruit) console.log(v);

Не совсем так удобно, как следовало бы, но это выполнимо, особенно если у вас есть несколько объектов:

var instruments={
    'a': 'accordion',
    'b': 'banjo',
    'c': 'cor anglais'
};
Object.defineProperty(instruments,Symbol.iterator,iterableProperties);
for(let v of instruments) console.log(v);

И так как каждый имеет право на мнение, я не могу понять, почему Объекты еще не повторяемы. Если вы можете заполнить их, как указано выше, или использовать for … in то я не вижу простой аргумент.

Одно из возможных предположений заключается в том, что итеративный объект является типом объекта, поэтому возможно, что итерируемый объект был ограничен подмножеством объектов на случай, если некоторые другие объекты взорвутся при попытке.