Понять разницу между циклом по объекту и циклом по массиву

Быстрый вопрос. Вы не можете перебрать {x: 1, y:2} с помощью цикла for-of. Почему?

const obj = { x: 1, y: 2 };
for (const key of obj) {
  // ERROR
  console.log(key);
}

Если вы не знаете точную причину, вы попали в нужное место! В этом посте я расскажу о том, что можно повторить.

Давайте сначала посмотрим, что можно зациклить

Перво-наперво. В JavaScript есть некоторые значения, которые можно перебирать с помощью for-of, а некоторые значения нельзя перебирать. Как правило, вы можете использовать for-of с массивами.

Струны также можно перематывать.

Но эти значения нельзя переоценить.

Как только вы выполните for-of, который выдает ошибку, вы можете увидеть, что сообщение об ошибке выглядит странно.

Uncaught TypeError: 123 is not iterable

Ни одно из имен переменных не повторяется, но что это означает?

Итератор - это тот, который вам действительно нужен

Начиная с ES6, была добавлена ​​новая функция Symbol, которая предназначена для уникального значения. В функции Symbol есть объект, который вы можете вызвать, Symbol.iterator. Это функция, которая возвращает итератор по умолчанию для объекта.

Когда объект создается, JavaScript внутренне создает объект с именем %IteratorPrototype и назначает его объекту-итератору вновь созданного объекта.

Когда этот объект используется, например, в цикле for-of, JavaScript проверяет, есть ли у него объект-итератор, который наследуется от %IteratorPrototype, внутреннего базового прототипа JavaScript для итерации. Если его не существует, выдается следующая ошибка.

Uncaught TypeError: 123 is not iterable

Это означает, что %IteratorPrototype должен использоваться объектом при его создании, чтобы его можно было зациклить, а объект-итератор объекта, например [1, 2, 3], называется Symbol.iterator - его внутренний термин в спецификации - %%iterator, но вы можете ' Не используйте это имя в своем коде.

Если Тип (O) не Object, выбросить TypeError исключение.

Symbol.iterator

У каждого отображаемого объекта есть Symbol.iterator - Array, String, TypedArray, Map и Set. Вы можете явно использовать функцию итератора, чтобы перебрать его.

Когда вы получаете объект-итератор, он выглядит так.

{
  __proto__: {
    next: f next(),
    Symbol(Symbol.toStringTag): "String Iterator"
  }
}

У него есть функция next, которая перемещает заголовок указателя индекса объекта, и метод Symbol.toStringTag, который определяет, как он распечатывает значение при вызове его toString.

Если вы вызываете next, он возвращает это.

{
  value: "h",
  done: false
}

done - это свойство, которое показывает, все ли индексы посещаются итератором, а value - это значение текущего индекса.

Если вы снова вызовете next, возвращаемое значение будет следующим.

{
  value: "e",
  done: false
}

Теперь вы знаете, почему value равно “e”.

Вы можете продолжать звонить next и проверять, что value будет «l», «l» и «o», таким образом, это «привет». Если вы вызовете next еще раз, возвращаемое значение будет немного другим.

{
  value: undefined,
  done: true
}

value теперь не определено, а done равно true.

Как только итератор перебирает все индексы строк или массивов или что-либо, что имеет Symbol.iterator, его заголовок, который указывал на индексы, теперь указывает на пустое пространство.

Таким образом, value будет неопределенным. И done заменяется на true, так как голова указывает на пустое пространство.

Тем не менее, вы можете продолжать звонить next, он просто даст тот же ответ, где value не определено, а done истинно.

Сделайте объект итерируемым

Теперь давайте создадим итерируемый объект, используя Symbol.iterator.

const obj = { x: 1, y: 2 };

Это был оригинальный объект. Что я собираюсь сделать, так это добавить функцию [Symbol.iterator], чтобы сделать ее циклической.

Код выглядит довольно запутанным, но я объясню вам, как это работает.

next() имеет область действия для this, которая относится к next(). Итак, this в next() не будет смотреть на obj.

Итак, во-первых, я использовал Object.entries(), чтобы получить каждую запись в obj.

// Object.entries(obj)
[
  0: ["x", 1],
  1: ["y", 2],
  length: 2
]

Затем я создал свойство index для обхода entries - i: 0.

if (this.i >= this.entries.length) {
  return { done: true, value: undefined };
}

Если i равно или больше длины entries, что означает, что итератор завершает цикл по entries, он начинает возвращать { done: true, value: undefined } из следующего вызова итератора.

Пока не будет выполнено указанное выше условие, возвращаемое значение будет:

{
  done: false,
  value: {
    [this.entries[this.i][0]]: this.entries[this.i++][1]
  }
}

Теперь данные будут успешно напечатаны.

const (let k of obj) {
  console.log(k);
}
// { x: 1 }
// { y: 2 }

Заключение

Фактором, определяющим, является ли объект в JavaScript циклическим, является то, есть ли у него Symbol.iterator внутри. Только объекты, которые имеют Symbol.iterator, могут быть зациклены - обычно массив.

Таким образом, такие объекты, как { x: 1, y: 2 }, не могут быть отображены, но есть способ сделать их отображаемыми. Вы можете добавить к ним индивидуальный next() метод, который я лично не рекомендую - это слишком много работы.

Ресурсы