Почему объекты не повторяемы в JavaScript?

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

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

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

Оператор for... of создает цикл, перебирающий итерируемые объекты (включая Array, Map, Set, arguments, object и т. Д.)...

Например, используя функцию генератора 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):

выход хаки для... из

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

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

Например:

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

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

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

5 ответов

Я попробую. Обратите внимание, что я не связан с 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 Работа?

Справочная документация полезна для этой части. В частности, объект считается iterable если он определяет Symbol.iterator имущество.

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

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

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

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

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

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

редактировать

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

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

var array = [...myIterable];

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

Мы только что изучили уровень программы xs, Поскольку в массивах хранятся последовательности данных, нас регулярно интересует только уровень данных. for..in очевидно, не имеет смысла в связи с массивами и другими "ориентированными на данные" структурами в большинстве случаев. Это причина, почему ES2015 представил for..of и повторяемый протокол.

Итерация уровня данных

Означает ли это, что мы можем просто отличить данные от уровня программы, различая функции от примитивных типов? Нет, потому что функции также могут быть данными в Javascript:

  • Array.prototype.sort например, ожидает, что функция выполняет определенный алгоритм сортировки
  • Гремит как () => 1 + 2 просто функциональные обертки для лениво оцененных значений

Кроме того, примитивные значения могут также представлять уровень программы:

  • [].length например, является Number но представляет длину массива и, таким образом, принадлежит к программной области

Это означает, что мы не можем различить программу и уровень данных, просто проверяя типы.


Важно понимать, что реализация итерационных протоколов для простых старых объектов Javascript будет зависеть от уровня данных. Но, как мы только что видели, надежное различие между данными и итерациями на уровне программы невозможно.

С Array Это различие тривиально: каждый элемент с целочисленным ключом является элементом данных. Object имеют эквивалентную особенность: enumerable дескриптор. Но действительно ли на это можно положиться? Я верю, что это не так! Значение enumerable дескриптор слишком размытый.

Заключение

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

Если свойства объекта были повторяемыми по умолчанию, программа и уровень данных были перепутаны. Так как каждый составной тип в Javascript основан на простых объектах, это будет применяться для Array а также Map также.

for..in, Object.keys, Reflect.ownKeys и т. д. можно использовать как для отражения, так и для итерации данных, четкое разграничение регулярно невозможно. Если вы не будете осторожны, вы быстро получите метапрограммирование и странные зависимости. Map абстрактный тип данных эффективно завершает объединение уровня программы и данных. я верю Map является наиболее значительным достижением в ES2015, даже если Promise с гораздо более захватывающим.

Меня тоже беспокоил этот вопрос.

Тогда мне пришла в голову идея использовать Object.entries({...}), он возвращает Array который является Iterable.

Кроме того, доктор Аксель Раушмайер опубликовал отличный ответ по этому поводу. См. Почему простые объекты НЕ повторяются

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

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

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

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

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

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

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

Технически, это не ответ на вопрос, почему? но я адаптировал ответ Джека Слокума выше в свете комментариев 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 тогда я не вижу простой аргумент.

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

Другие вопросы по тегам