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