Чому об’єкти не піддаються ітерації в JavaScript?


87

Чому за замовчуванням об’єкти не можна повторити?

Я постійно бачу запитання, пов’язані з ітерацією об’єктів, загальним рішенням є перегляд властивостей об’єкта та доступ до значень в об’єкті таким чином. Це здається настільки звичним, що змушує задуматися, чому самі об’єкти не піддаються ітерації.

Такі твердження, як 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 ):

вихід хакі для ... оф

За замовчуванням {}об'єкти не піддаються ітерації, але чому? Чи переважуватимуть недоліки потенційні переваги об'єктів, які можна повторити? Які проблеми пов'язані з цим?

Крім того, оскільки {}об'єкти відрізняються від «Array-як» колекція і «ітерація об'єктів» , таких як 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

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/

Редагувати

І у відповідь на ваше оновлене запитання, так, можна перетворити an 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 ... додається для усунення недоліків, пов'язаних з конструкцією for ...". Ні. Він був доданий для підтримки загального способу повторення будь-чого, і є частиною широкого набору нових функцій, включаючи самі ітерабелі, генератори, а також карти та набори. Навряд чи це задумано як заміну або оновлення до 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, навіть якщо Promises набагато цікавіші .


3
+1, я думаю: "Немає суттєвого способу реалізації ітераційних протоколів для об’єктів, оскільки не кожен об’єкт є колекцією". підсумовує це.
Чарлі Шліссер,

1
Я не думаю, що це хороший аргумент. Якщо ваш об'єкт не є колекцією, чому ви намагаєтеся прокрутити його? Немає значення, що не кожен об’єкт є колекцією, оскільки ви не будете намагатися перебирати ті, що не є.
BT

Насправді кожен об’єкт є колекцією, і мова не вирішує, чи є колекція цілісною чи ні. Масиви та Карти також можуть збирати не пов’язані між собою значення. Справа в тому, що ви можете переглядати клавіші будь-якого об’єкта незалежно від їх використання, тож ви знаходитесь на кроці від ітерації їх значень. Якби ви говорили про мову, яка статично набирає значення масиву (або будь-якої іншої колекції), ви можете говорити про такі обмеження, але не про JavaScript.
Маннго,

Той аргумент, що кожен об’єкт не є колекцією, не має сенсу. Ви припускаєте, що ітератор має лише одну мету (ітерацію колекції). Ітератор за замовчуванням для об’єкта буде ітератором властивостей об’єкта, незалежно від того, що ці властивості представляють (колекція чи щось інше). Як сказав Маннго, якщо ваш об'єкт не представляє колекцію, тоді програміст повинен не поводитися з нею як з колекцією. Можливо, вони просто хочуть повторити властивості об'єкта для деяких результатів налагодження? Є безліч інших причин, крім колекції.
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
завдяки торазабуро. я переглянув своє запитання. я хотів би бачити приклад, який би використовував [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

Це найновіший підхід (який працює на хромованій канарці)

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

редагувати2

Повернімось до вашого початкового прикладу, якщо ви хочете скористатися наведеним вище методом прототипу, він би хотів цього

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


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.