Цикл FOR IN в JavaScript затрагивает несколько концепций, которые на первый взгляд могут быть не очевидны при быстром изучении языка. В них обязательно нужно разобраться. Надеюсь эта публикация откроет глаза на лучшее понимание работы языка JavaScript.
Оглавление
- В чём основная суть работы цикла FOR IN в JavaScript?
- Видео на тему работы цикла FOR IN в JavaScript
- Обход собственных свойств простого объекта JavaScript
- Может ли свойство быть недоступно циклу FOR IN в JavaScript?
- Атрибуты свойств объектов в JavaScript
- Наследование свойств от объекта к объекту и цикл FOR IN в JavaScript
- Цикл FOR IN с массивом (Array) в JavaScript
- Цикл FOR IN с функцией (Function) в JavaScript
- Получение имён ключей глобального объекта JavaScript
- Цикл FOR IN и символьные ключи объектов в JavaScript
В чём основная суть работы цикла FOR IN в JavaScript?
Цикл FOR IN — это про перебирание ключей у объектов, то есть про всех возможных представителей типа Object. У объектов есть свойства. У свойств есть ключи и значения. Так вот задача цикла FOR IN пройтись по всем свойствам ЦЕЛЕВОГО ОБЪЕКТА и собрать имена свойств (имена ключей), которые имеют тип String.
Выбирая имя свойства на каждой своей итерации, можно обращаться к значениям объекта. А можно просто выводить списки имён ключей для дальнейшего анализа объекта. Можно использовать цикл для указания количества шагов, которое нужно совершить над другим объектом.
Важно здесь то, что цикл FOR IN не работает с ключами объектов, которые имеют примитивное значение Symbol.
Видео на тему работы цикла FOR IN в JavaScript
Обход собственных свойств простого объекта JavaScript
Начнём с простого примера. У нас есть целевой объект:
let myCar = {
car_type: «седан«,
car_color: «белый«,
car_year: 2022,
car_max_speed: 180,
car_max_fuel: 60
}
Если мы пройдёмся циклом FOR IN по этому объекту в браузере, то мы сможем получать на каждой итерации имя свойства (имя ключа) нашего объекта myCar и выводить его в консоль. Это мы делаем только для того, чтобы убедиться что всё работает так, как мы и предполагали.
for(let property_name in myCar){
console.log(property_name);
}
Результат работы цикла (получаем имена ключей свойств объекта myCar):
Находясь в консоли браузера, мы пишем код на самом верхнем уровне области видимости. Можно сказать, что весь наш код с «голым» циклом находится в одной глобальной функции и выполняется при подаче команды. То есть как-будто бы в среде выполнения кода находится всего одна функция и она постоянно вызывается при подаче команды. По стандарту ECMAScript такой файл называется Script. Но нужно знать, что бывают и подключаемые файлы (не основные), которые называются Module.
Всю предыдущую историю можно сделать в более практическом варианте. Например, можно сложить все свойства этого объекта в какой-нибудь массив. И сделаем мы это через объявление отдельной функции и отдельного массива внутри неё. Изначально массив будет объявлен литерально и он будет пуст. Возвращать будем массив с именами свойств в строковом виде для передаваемого объекта.
function getAllPropertyName(object){
let arr = [];
for(let p_key in object){arr.push(p_key)};
return arr;
}
Функция объявлена. Теперь вызовем её с нашим объектом автомобиля:
getAllPropertyName(myCar)
Теперь имена свойств (ключи целевого объекта) красиво лежат в одном удобном массиве. Их можно использовать для своих собственных нужд и алгоритмов обработки. Можно подсчитывать их количество, можно сортировать их, а можно производить фильтрацию. Всё зависит от структуры данных объекта и его системы именования ключей.
Может ли свойство быть недоступно циклу FOR IN в JavaScript?
Как вы думаете? Может ли возникнуть такая ситуация, при которой нужно ограничить возможность отдачи какого-нибудь имени свойства у объекта? Ответ — может.
Представьте, что мы хотим спрятать некоторое свойство от перебора, чтобы его имя не попадало в работу цикла FOR IN. Как это сделать?
Атрибуты свойств объектов в JavaScript
Следующая важная концепция — это атрибуты свойств объектов. (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 итератора не более одного раза в любом перечислении.
Наследование свойств от объекта к объекту и цикл FOR IN в JavaScript
Как обстоит ситуация с прототипами?
Перечисление свойств целевого объекта включает рекурсивное перечисление свойств его прототипа, прототипа прототипа и так далее; но свойство прототипа не обрабатывается, если оно имеет то же имя, что и свойство, которое уже было обработано.
Давайте на практике в этом убедимся:
Обратите внимание! В трёх объектах было объявлено суммарно 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 или объекта в его цепочке прототипов изменяется.
Цикл FOR IN с массивом (Array) в JavaScript
Массивы это тоже объекты. А значит и у массивов можно получить перечисляемые ключи свойств.
let arrayF = ['ssd','fggf','cvc','vv']
Вызываем цикл for in
for(x in arrayF){console.log(x)}
Результат вызова в консоли браузера:
Что тут произошло? Все массивы обладают парами — ключ/значение. Почти также как и у объектов. Основное отличие массива от объекта в том, что массив — это НЕИМЕНОВАННЫЙ СПИСОК, в то время как объект — это ИМЕНОВАННЫЙ СПИСОК.
Если в объекте ключи называются более менее логичными именами, то в массивах это тупо целочисленные индексы.
То есть вместо привычных слов мы просто получаем строковое представление индексов массива. НЕ ЧИСЛА!! А СТРОКИ, которые похожи на числа!!!
Цикл FOR IN с функцией (Function) в JavaScript
С функцией уже интереснее. Функции это тоже объекты.
function sumABC(a,b,c){return a+b+c}; for(x in sumABC){console.log(x)}; undefined
У всех объектов-функций есть свои собственные свойства. Но при попытке обойти их циклом for in, мы не получаем ничего. Точнее получаем undefined.
Почему так происходит?
Всё дело в атрибутах свойств объекта-функции. Все атрибуты типа enumerable имеют значение false.
Именно поэтому мы не получаем абсолютно ничего.
Тогда второй вопрос. Почему «ничего»? А где же перечисляемые свойства прототипов объекта-функции?
Все объекты-функции «прототипируются» от объекта Object. Так вот все свойства объекта Object являются НЕ-ПЕРЕЧИСЛЯЕМЫМИ. Именно поэтому мы не получаем ключи свойств прототипа. И в итоге вообще ничего. Вот такие функции интересные создания.
Получение имён ключей глобального объекта
Цикл FOR IN можно использовать для обхода свойств глобального объекта на клиенте:
let arr = []; for(let keyname in window){arr.push(keyname)}; console.log(arr);
Результат работы цикла FOR IN:
Так можно получить полный список имён ключей, которые доступны к перечислению — то есть те, у которых атрибут enumerable
свойства имеет значение true
.
Цикл FOR IN и символьные ключи объектов в JavaScript
Среди примитивных значений встречается тип Symbol. Этим типом можно именовать ключи свойств объектов.
Но проблема заключается в том, что цикл FOR IN такие ключи просто не видит.
let obj = {q:1} undefined let sym = Symbol('w') undefined obj[sym] = 77 77 obj {q: 1, Symbol(w): 77}q: 1Symbol(w): 77[[Prototype]]: Object for(let x in obj){console.log(x)}; q
Результат выполнения цикла:
Мы получили только строковый ключ «q». Символьный ключ был проигнорирован циклом FOR IN.
Забавно здесь то, что атрибут enumerable
для символьного свойства имеет значение true — доступен к перечислению. Но по факту этого не происходит.