JavaScript | Цикл for in — efim360.ru

JavaScript | Цикл for in

Цикл 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);

}

Результат работы цикла (получаем ключи свойств):

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

Находясь в консоли браузера, мы пишем код на самом верхнем уровне области видимости. Можно сказать, что весь наш код с "голым" циклом находится в одной глобальной функции и выполняется при подаче команды. То есть как-будто бы в среде выполнения кода находится всего одна функция и она постоянно вызывается при подаче команды.


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

function getAllPropertyName(object){

    let arr = [];

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

    return arr;

}

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

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

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

 

Должен ли ключ свойства возвращаться как значение, циклом 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)

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

Получение дескрипторов свойств целевого объекта - 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 итератора не более одного раза в любом перечислении.

 

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

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

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

Получили свойства целевого объекта, а также всех его прототипов по цепочке - 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 или объекта в его цепочке прототипов изменяется.

 

Ситуация с массивом (Array)

Массивы это тоже объекты. А значит и у массивов можно получить перечисляемые ключи свойств.

let arrayF = ['ssd','fggf','cvc','vv']

Вызываем цикл for in

for(x in arrayF){console.log(x)}

Результат вызова в консоли браузера:

Получили индексы массива в строковом представлении циклом for in - JavaScript
Получили индексы массива в строковом представлении циклом for in - JavaScript

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

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

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

 

Ситуация с функцией (Function)

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

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 являются НЕ-ПЕРЕЧИСЛЯЕМЫМИ. Именно поэтому мы не получаем ключи свойств прототипа. И в итоге вообще ничего. Вот такие функции интересные создания.

 

Поделись записью