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


87

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

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

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

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

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

вывод hacky для ... из

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

Кроме того, поскольку {}объекты отличаются от «Array-как» коллекция и «итерация объектов» , таких как 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
Все, что имеет Symbol.iteratorсвойство, можно повторять. Так что вам просто нужно реализовать это свойство. Одно из возможных объяснений того, почему объекты не являются итерируемыми, может заключаться в том, что это будет означать, что все было итеративным, поскольку все является объектом (кроме, конечно, примитивов). Однако что означает перебор функции или объекта регулярного выражения?
Феликс Клинг,

7
Какой у вас настоящий вопрос? Почему ECMA приняла такие решения?
Стив Беннетт

3
Поскольку у объектов НЕТ гарантированного порядка их свойств, мне интересно, не противоречит ли это определению итерируемого объекта, который, как вы ожидаете, будет иметь предсказуемый порядок?
jfriend00

2
Чтобы получить авторитетный ответ на вопрос «почему», спросите на esdiscuss.org
Феликс Клинг,

1
@FelixKling - это пост про ES6? Вам, вероятно, следует отредактировать его, чтобы указать, о какой версии вы говорите, потому что «будущая версия ECMAScript» со временем не будет работать очень хорошо.
jfriend00,

Ответы:


42

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

Почему бы просто не сделать obj[Symbol.iterator] = obj[Symbol.enumerate]в вашем последнем примере?
Берги

@Bergi - потому что я не видел этого в документации (и не вижу описанного здесь свойства ). Хотя одним из аргументов в пользу явного определения итератора является то, что он упрощает выполнение определенного порядка итераций, если это потребуется. Однако если порядок итераций не важен (или если порядок по умолчанию подходит) и однострочный ярлык работает, нет особых причин не использовать более сжатый подход.
Aroth

К сожалению, [[enumerate]]это не общеизвестный символ (@@ enumerate), а внутренний метод. Я бы должен был бытьobj[Symbol.iterator] = function(){ return Reflect.enumerate(this) }
Берги

Какая польза от всех этих предположений, если сам процесс обсуждения хорошо задокументирован? Очень странно, что вы скажете: «Итак, я предполагаю, что конструкция for ... of добавляется для устранения недостатков, связанных с конструкцией for ... in». Нет. Он был добавлен для поддержки общего способа итерации чего-либо и является частью широкого набора новых функций, включая сами итерации, генераторы, карты и наборы. Вряд ли это означает замену или обновление до for...in, имеющего другую цель - перебирать свойства объекта.

2
Хороший момент, еще раз подчеркнув, что не каждый объект является коллекцией. Объекты использовались как таковые в течение долгого времени, потому что это было очень удобно, но в конечном итоге это не совсем коллекции. Вот что у нас есть Mapна данный момент.
Феликс Клинг,

9

Objects не реализуют протоколы итераций в 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 будет зависеть от уровня данных. Но, как мы только что убедились, достоверное различие между итерацией на уровне данных и программным обеспечением невозможно.

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

Вывод

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

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

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


3
+1, я думаю, что «нет значимого способа реализовать протоколы итерации для объектов, потому что не каждый объект является коллекцией». подводит итог.
Чарли Шлиссер

1
Не думаю, что это хороший аргумент. Если ваш объект не является коллекцией, почему вы пытаетесь перебрать его? Не имеет значения, что не каждый объект является коллекцией, потому что вы не будете пытаться перебирать другие объекты.
BT

Фактически, каждый объект является коллекцией, и не язык решает, является ли эта коллекция последовательной. Массивы и карты также могут собирать несвязанные значения. Дело в том, что вы можете перебирать ключи любого объекта независимо от их использования, так что вы находитесь в одном шаге от перебора их значений. Если вы говорили о языке, который статически вводит значения массива (или любой другой коллекции), вы могли бы говорить о таких ограничениях, но не о JavaScript.
Manngo 08

Этот аргумент, что каждый объект не является коллекцией, не имеет смысла. Вы предполагаете, что итератор имеет только одну цель (итерация коллекции). Итератор по умолчанию для объекта будет итератором свойств объекта, независимо от того, что эти свойства представляют (будь то коллекция или что-то еще). Как сказал Маннго, если ваш объект не представляет собой коллекцию, то программист не должен рассматривать его как коллекцию. Может быть, они просто хотят перебрать свойства объекта для отладки? Помимо коллекции есть много других причин.
jfriend00

8

Думаю, должен возникнуть вопрос: «Почему нет встроенной итерации объекта?»

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

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

1
спасибо torazaburo. я пересмотрел свой вопрос. Я хотел бы увидеть пример использования, [Symbol.iterator]а также, если бы вы могли расширить эти непредвиденные последствия.
бумбокс

4

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

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

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

2

Это последний подход (который работает в 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);
    }
}

2

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

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

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


0

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

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тогда я не вижу простого аргумента.

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.