Цикл for in в JavaScript затрагивает несколько концепций, которые на первый взгляд могут быть не очевидны при быстром изучении языка. В них обязательно нужно разобраться. Надеюсь эта публикация откроет глаза на лучшее понимание работы языка JavaScript.
В чём основная суть работы цикла for in?
Цикл for in — это про объекты, то есть про всех возможных представителей типа Object. У объектов есть свойства. У свойств есть ключи и значения. Так вот задача цикла for in пройтись по всем свойствам ЦЕЛЕВОГО ОБЪЕКТА и собрать имена свойств, которые имеют тип String.
Простой пример
Начнём с простого примера. У нас есть целевой объект:
let myCar = {
car_type: «седан«,
car_color: «белый«,
car_year: 2022,
car_max_speed: 180,
car_max_fuel: 60
}
Если мы пройдёмся циклом for in по этому объекту в браузере, то мы можем получать на каждой итерации имя свойства нашего объекта myCar и выводить в консоль. Это мы делаем только для того, чтобы убедиться что всё работает так, как мы и предполагали.
for(property_name in myCar){
console.log(property_name);
}
Результат работы цикла (получаем ключи свойств):

Находясь в консоли браузера, мы пишем код на самом верхнем уровне области видимости. Можно сказать, что весь наш код с «голым» циклом находится в одной глобальной функции и выполняется при подаче команды. То есть как-будто бы в среде выполнения кода находится всего одна функция и она постоянно вызывается при подаче команды.
Всю предыдущую историю можно сделать в более практическом варианте. Например, можно сложить все свойства этого объекта в какой-нибудь массив. И сделаем мы это через объявление отдельной функции и отдельного массива внутри неё. Изначально массив будет объявлен литерально и он будет пуст. Возвращать будем массив с именами свойств передаваемого объекта.
function getAllPropertyName(object){
let arr = [];
for(p_key in object){arr.push(p_key)};
return arr;
}
Функция объявлена. Теперь вызовем её с нашим объектом автомобиля:
getAllPropertyName(myCar)

Теперь имена свойств (ключи целевого объекта) красиво лежат в одном удобном массиве. Их можно использовать для своих собственных нужд и алгоритмов обработки.
Должен ли ключ свойства возвращаться как значение, циклом for in?
Как вы думаете? Может ли возникнуть такая ситуация, при которой нужно ограничить возможность отдачи какого-нибудь имени свойства у объекта? Ответ — может.
Представьте, что мы хотим спрятать некоторое свойство от перебора, чтобы его имя не попадало в работу цикла for in. Как это сделать?
Следующая важная концепция — это атрибуты свойств объектов. (ECMAScript — 6.1.7.1 Property Attributes)
Взгляните на таблицу:
Имя атрибута | Типы свойства, для которых оно присутствует | Область значений | Значение по умолчанию | Описание |
---|---|---|---|---|
[[Value]] | свойство данных | Любой тип из языка ECMAScript | undefined | Значение, полученное при доступе к свойству. |
[[Writable]] | свойство данных | Boolean | false | Если false, попытки кода ECMAScript изменить атрибут свойства [[Value]] с помощью [[Set]] не увенчаются успехом. |
[[Get]] | свойство доступа | Object или undefined | undefined | Если значение является объектом Object, это должен быть объект функции. Внутренний метод функции [[Call]] (Таблица 5) вызывается с пустым списком аргументов для получения значения свойства каждый раз, когда выполняется доступ к свойству. |
[[Set]] | свойство доступа | Object или undefined | undefined | Если значение является объектом, это должен быть объект функции. Внутренний метод функции [[Call]] (Таблица 5) вызывается со списком аргументов, содержащим назначенное значение в качестве единственного аргумента каждый раз, когда выполняется набор доступа к свойству.
Эффект внутреннего метода свойства [[Set]] может, но не обязательно, влиять на значение, возвращаемое последующими вызовами внутреннего метода свойства [[Get]]. |
[[Enumerable]] | свойство данных или свойство доступа | Boolean | false | Если значение равно true, свойство будет перечислено с помощью for-in перечисления (см. 14.7.5). В противном случае свойство считается не подлежащим перечислению. |
[[Configurable]] | свойство данных или свойство доступа | Boolean | false | Если значение равно false, попытки удалить свойство, изменить его со свойства данных на свойство доступа или со свойства доступа на свойство данных или внести какие-либо изменения в его атрибуты (кроме замены существующего [[Value]] или установки [[Writable]] на false) завершатся неудачей. |
Таблица 3: Атрибуты свойства Object — Table 3: Attributes of an Object property
Все шесть атрибутов влияют на то, как «под капотом» работают свойства наших объектов к которым мы так любим обращаться.
Давайте посмотрим на наш объект автомобиля со стороны атрибутов свойств. Сделать это можно при помощи конструктора Object и его метода getOwnPropertyDescriptors ( O )
Object.getOwnPropertyDescriptors(myCar)
Результат вызова — объект из объектов:

Обратите внимание, что у всех свойств целевого объекта атрибут [[Enumerable]] установлен в логическое значение истины — true.
Это означает то, что все свойства целевого объекта являются ПЕРЕЧИСЛЯЕМЫМИ и могут участвовать в цикле for in. Мы действительно получили все названия ключей, когда первый раз вызвали цикл.
Давайте теперь проверим теоретическую часть и поменяем атрибут перечисления enumerable у свойства car_max_speed на false. Как это сделать? Сделать это можно при помощи конструктора Object и его метода defineProperty ( O, P, Attributes )
Object.defineProperty(myCar, "car_max_speed", {enumerable: false})

Обратите внимание, как в консоли браузер Google Chrome затемнил свойство «car_max_speed«, как бы намекая, что оно отличается от остальных.
Снова вызываем нашу функцию с циклом for in.

Пробегая по нашему целевому объекту, цикл перешагнул через свойство «car_max_speed» и вернул результат на одно значение меньше, чем в первый вызов. Вот это поворот! Свойство максимальной скорости автомобиля спряталось.
Мы проверили ситуацию, когда цикл for in может пропускать возвращение ключа свойства объекта.
Информация из стандарта ECMAScript
- Возвращённые ключи свойств не включают ключи, которые являются символами (Symbols).
- Свойства целевого объекта могут быть удалены во время перечисления. Свойство, которое удаляется до того, как оно будет обработано методом next итератора, игнорируется.
- Если новые свойства добавляются к целевому объекту во время перечисления, не гарантируется, что новые добавленные свойства будут обработаны в активном перечислении (active enumeration). Имя свойства будет возвращено методом next итератора не более одного раза в любом перечислении.
А как обстоит ситуация с прототипами?
Перечисление свойств целевого объекта включает рекурсивное перечисление свойств его прототипа, прототипа прототипа и так далее; но свойство прототипа не обрабатывается, если оно имеет то же имя, что и свойство, которое уже было обработано.
Давайте на практике в этом убедимся:

Обратите внимание! В трёх объектах было объявлено суммарно 6 свойств, но после работы цикла for in мы получили всего 5. Что произошло?
Когда цикл впервые обнаружил в целевом объекте свойство с ключом name, тогда он добавил его в набор уникальных строк. Этот набор невидим для нас с вами, но он работает внутри алгоритма цикла. Об этом просто нужно знать. В результате, свойство name у объекта-прототипа bbb не попадает в итоговый результат (в вывод в консоль браузера).
Цикл работает на погружение. Вычерпывая перечисляемые свойства одного объекта, цикл погружается на уровень глубже в другой объект и так далее….пока не закончатся прототипы или перечисляемые свойства.
Если мы отключим возможность перечисления у свойства voice объекта bbb, тогда оно не попадёт в результат работы цикла for in. И тогда количество выводов уменьшится на один. Всего станет 4 итерации у цикла. Проверяем:

Информация из стандарта ECMAScript
Значения атрибутов [[Enumerable]] не учитываются при определении того, было ли уже обработано свойство объекта-прототипа. Имена перечислимых свойств объектов-прототипов должны быть получены путем вызова EnumerateObjectProperties, передавая объект-прототип в качестве аргумента. EnumerateObjectProperties должен получить собственные ключи свойств целевого объекта, вызвав его внутренний метод [[OwnPropertyKeys]]. Атрибуты свойства целевого объекта должны быть получены путём вызова его внутреннего метода [[GetOwnProperty]].
Кроме того, если ни O, ни какой-либо объект в его цепочке прототипов не является экзотическим объектом Proxy, экзотическим объектом с целочисленным индексом, экзотическим объектом пространства имен модуля или реализацией, предоставленной экзотическим объектом, то итератор должен вести себя так же, как итератор, заданный CreateForInIterator(O) до тех пор, пока не произойдет одно из следующих событий:
- значение внутреннего слота [[Prototype]] для O или объекта в его цепочке прототипов изменяется,
- свойство удаляется из O или объекта в его цепочке прототипов,
- свойство добавляется к объекту в цепочке прототипов O, или
- значение атрибута [[Enumerable]] свойства объекта O или объекта в его цепочке прототипов изменяется.
Ситуация с массивом (Array)
Массивы это тоже объекты. А значит и у массивов можно получить перечисляемые ключи свойств.
let arrayF = ['ssd','fggf','cvc','vv']
Вызываем цикл for in
for(x in arrayF){console.log(x)}
Результат вызова в консоли браузера:

Что тут произошло? Все массивы обладают парами — ключ/значение. Почти также как и у объектов. Основное отличие массива от объекта в том, что массив — это НЕИМЕНОВАННЫЙ СПИСОК, в то время как объект — это ИМЕНОВАННЫЙ СПИСОК.
Если в объекте ключи называются более менее логичными именами, то в массивах это тупо целочисленные индексы.
То есть вместо привычных слов мы просто получаем строковое представление индексов массива. НЕ ЧИСЛА!! А СТРОКИ, которые похожи на числа!!!
Ситуация с функцией (Function)
С функцией уже интереснее. Функции это тоже объекты.
function sumABC(a,b,c){return a+b+c}; for(x in sumABC){console.log(x)}; undefined
У всех объектов-функций есть свои собственные свойства. Но при попытке обойти их циклом for in, мы не получаем ничего. Точнее получаем undefined.

Почему так происходит?
Всё дело в атрибутах свойств объекта-функции. Все атрибуты типа enumerable имеют значение false.

Именно поэтому мы не получаем абсолютно ничего.
Тогда второй вопрос. Почему «ничего»? А где же перечисляемые свойства прототипов объекта-функции?

Все объекты-функции «прототипируются» от объекта Object. Так вот все свойства объекта Object являются НЕ-ПЕРЕЧИСЛЯЕМЫМИ. Именно поэтому мы не получаем ключи свойств прототипа. И в итоге вообще ничего. Вот такие функции интересные создания.