JavaScript | Цикл FOR IN

JavaScript | Цикл FOR IN

Цикл FOR IN в JavaScript затрагивает несколько концепций, которые на первый взгляд могут быть не очевидны при быстром изучении языка. В них обязательно нужно разобраться. Надеюсь эта публикация откроет глаза на лучшее понимание работы языка JavaScript.

 

Оглавление

  1. В чём основная суть работы цикла FOR IN в JavaScript?
  2. Видео на тему работы цикла FOR IN в JavaScript
  3. Обход собственных свойств простого объекта JavaScript
  4. Может ли свойство быть недоступно циклу FOR IN в JavaScript?
  5. Атрибуты свойств объектов в JavaScript
  6. Наследование свойств от объекта к объекту и цикл FOR IN в JavaScript
  7. Цикл FOR IN с массивом (Array) в JavaScript
  8. Цикл FOR IN с функцией (Function) в JavaScript
  9. Получение имён ключей глобального объекта JavaScript
  10. Цикл 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):

Простой цикл for in - JavaScript
Простой цикл for in — JavaScript

Находясь в консоли браузера, мы пишем код на самом верхнем уровне области видимости. Можно сказать, что весь наш код с «голым» циклом находится в одной глобальной функции и выполняется при подаче команды. То есть как-будто бы в среде выполнения кода находится всего одна функция и она постоянно вызывается при подаче команды. По стандарту ECMAScript такой файл называется Script. Но нужно знать, что бывают и подключаемые файлы (не основные), которые называются Module.

Всю предыдущую историю можно сделать в более практическом варианте. Например, можно сложить все свойства этого объекта в какой-нибудь массив. И сделаем мы это через объявление отдельной функции и отдельного массива внутри неё. Изначально массив будет объявлен литерально и он будет пуст. Возвращать будем массив с именами свойств в строковом виде для передаваемого объекта.

function getAllPropertyName(object){

    let arr = [];

    for(let p_key in object){arr.push(p_key)};

    return arr;

}

Функция объявлена. Теперь вызовем её с нашим объектом автомобиля:

getAllPropertyName(myCar)
Вызвали функцию getAllPropertyName с циклом for in в теле - JavaScript
Вызвали функцию getAllPropertyName с циклом for in в теле — JavaScript

Теперь имена свойств (ключи целевого объекта) красиво лежат в одном удобном массиве. Их можно использовать для своих собственных нужд и алгоритмов обработки. Можно подсчитывать их количество, можно сортировать их, а можно производить фильтрацию. Всё зависит от структуры данных объекта и его системы именования ключей.

 

Может ли свойство быть недоступно циклу 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)

Результат вызова — объект из объектов:

Получение дескрипторов свойств целевого объекта - JavaScript
Получение дескрипторов свойств целевого объекта — JavaScript

Обратите внимание, что у всех свойств целевого объекта атрибут [[Enumerable]] установлен в логическое значение истины — true.

Это означает то, что все свойства целевого объекта являются ПЕРЕЧИСЛЯЕМЫМИ и могут участвовать в цикле for in. Мы действительно получили все названия ключей, когда первый раз вызвали цикл.

 

Давайте теперь проверим теоретическую часть и поменяем атрибут перечисления enumerable у свойства car_max_speed на false. Как это сделать? Сделать это можно при помощи конструктора Object и его метода defineProperty ( O, P, Attributes )

Object.defineProperty(myCar, "car_max_speed", {enumerable: false})
Изменили атрибут enumerable с true на false у свойства car_max_speed - JavaScript
Изменили атрибут enumerable с true на false у свойства car_max_speed — JavaScript

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

Снова вызываем нашу функцию с циклом for in.

Цикл for in прошёл мимо свойства с ключом car_max_speed - JavaScript
Цикл for in прошёл мимо свойства с ключом car_max_speed — JavaScript

Пробегая по нашему целевому объекту, цикл перешагнул через свойство «car_max_speed» и вернул результат на одно значение меньше, чем в первый вызов. Вот это поворот! Свойство максимальной скорости автомобиля спряталось.

Мы проверили ситуацию, когда цикл for in может пропускать возвращение ключа свойства объекта.

 

Информация из стандарта ECMAScript

  • Возвращённые ключи свойств не включают ключи, которые являются символами (Symbols).
  • Свойства целевого объекта могут быть удалены во время перечисления. Свойство, которое удаляется до того, как оно будет обработано методом next итератора, игнорируется.
  • Если новые свойства добавляются к целевому объекту во время перечисления, не гарантируется, что новые добавленные свойства будут обработаны в активном перечислении (active enumeration). Имя свойства будет возвращено методом next итератора не более одного раза в любом перечислении.

 

Наследование свойств от объекта к объекту и цикл FOR IN в JavaScript

Как обстоит ситуация с прототипами?

Перечисление свойств целевого объекта включает рекурсивное перечисление свойств его прототипа, прототипа прототипа и так далее; но свойство прототипа не обрабатывается, если оно имеет то же имя, что и свойство, которое уже было обработано.

Давайте на практике в этом убедимся:

Получили свойства целевого объекта, а также всех его прототипов по цепочке - JavaScript
Получили свойства целевого объекта, а также всех его прототипов по цепочке — JavaScript

Обратите внимание! В трёх объектах было объявлено суммарно 6 свойств, но после работы цикла for in мы получили всего 5. Что произошло?

Когда цикл впервые обнаружил в целевом объекте свойство с ключом name, тогда он добавил его в набор уникальных строк. Этот набор невидим для нас с вами, но он работает внутри алгоритма цикла. Об этом просто нужно знать. В результате, свойство name у объекта-прототипа bbb не попадает в итоговый результат (в вывод в консоль браузера).

Цикл работает на погружение. Вычерпывая перечисляемые свойства одного объекта, цикл погружается на уровень глубже в другой объект и так далее….пока не закончатся прототипы или перечисляемые свойства.

 

Если мы отключим возможность перечисления у свойства voice объекта bbb, тогда оно не попадёт в результат работы цикла for in. И тогда количество выводов уменьшится на один. Всего станет 4 итерации у цикла. Проверяем:

У прототипа есть не-перечисляемое свойство, for in его пропустил - JavaScript
У прототипа есть не-перечисляемое свойство, for in его пропустил — JavaScript

Информация из стандарта 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 - JavaScript
Получили индексы массива в строковом представлении циклом for in — JavaScript

Что тут произошло? Все массивы обладают парами — ключ/значение. Почти также как и у объектов. Основное отличие массива от объекта в том, что массив — это НЕИМЕНОВАННЫЙ СПИСОК, в то время как объект — это ИМЕНОВАННЫЙ СПИСОК.

Если в объекте ключи называются более менее логичными именами, то в массивах это тупо целочисленные индексы.

То есть вместо привычных слов мы просто получаем строковое представление индексов массива. НЕ ЧИСЛА!! А СТРОКИ, которые похожи на числа!!!

 

Цикл FOR IN с функцией (Function) в JavaScript

С функцией уже интереснее. Функции это тоже объекты.

function sumABC(a,b,c){return a+b+c};

for(x in sumABC){console.log(x)};

undefined

У всех объектов-функций есть свои собственные свойства. Но при попытке обойти их циклом for in, мы не получаем ничего. Точнее получаем undefined.

Цикл for in по функции вернул undefined - JavaScript
Цикл for in по функции вернул undefined — JavaScript

Почему так происходит?

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

Атрибут enumerable свойств функциональных объектов имеет значение false - JavaScript
Атрибут enumerable свойств функциональных объектов имеет значение false — JavaScript

Именно поэтому мы не получаем абсолютно ничего.

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

У объекта-прототипа Object нет перечисляемых свойств - JavaScript
У объекта-прототипа Object нет перечисляемых свойств — JavaScript

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

 

Получение имён ключей глобального объекта

Цикл FOR IN можно использовать для обхода свойств глобального объекта на клиенте:

let arr = [];
for(let keyname in window){arr.push(keyname)};
console.log(arr);

Результат работы цикла FOR IN:

Получили доступные к перечислению имена ключей глобального объекта циклом for in - JavaScript
Получили доступные к перечислению имена ключей глобального объекта циклом for in — JavaScript

Так можно получить полный список имён ключей, которые доступны к перечислению — то есть те, у которых атрибут 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

Результат выполнения цикла:

Цикл FOR IN пропускает символьные ключи - JavaScript
Цикл FOR IN пропускает символьные ключи — JavaScript

Мы получили только строковый ключ «q». Символьный ключ был проигнорирован циклом FOR IN.

Забавно здесь то, что атрибут enumerable для символьного свойства имеет значение true — доступен к перечислению. Но по факту этого не происходит.