Заметка о перебираемых объектах

от автора

Доброго времени суток, друзья!

Данная заметка не имеет особой практической ценности. С другой стороны, в ней исследуется некоторые «пограничные» возможности JavaScript, которые могут показаться вам интересными.

Руководство по стилю JavaScript от Goggle советует отдавать предпочтение циклу for-of там, где это возможно.

Руководство по стилю JavaScript от Airbnb не рекомендует использовать итераторы. Вместо циклов for-in и for-of следует использовать функции высшего порядка, такие как map(), every(), filter(), find(), findIndex(), reduce(), some() для итерации по массивам и Object.keys(), Object.values(), Object.entries() для итерации по массивам из объектов. Об этом позже.

Вернемся к Google. Что означает «там, где это возможно»?

Рассмотрим парочку примеров.

Допустим, у нас есть такой массив:

const users = ["John", "Jane", "Bob", "Alice"]; 

И мы хотим вывести в консоль значения его элементов. Как нам это сделать?

// вспомогательная функция log = (value) => console.log(value);  // for for (let i = 0; i < users.length; i++) {   log(users[i]); // John Jane Bob Alice }  // for-in for (const item in users) {   log(users[item]); }  // for-of for (const item of users) {   log(item); }  // forEach() users.forEach((item) => log(item));  // map() // побочный эффект - возвращает новый массив // поэтому в данном случае лучше использовать forEach() users.map((item) => log(item)); 

Все прекрасно работает без лишних усилий с нашей стороны.

Теперь предположим, что у нас есть такой объект:

const person = {   name: "John",   age: 30,   job: "developer", }; 

И мы хотим сделать тоже самое.

// for for (let i = 0; i < Object.keys(person).length; i++) {   log(Object.values(person)[i]); // John 30 developer }  // for-in for (const i in person) {   log(person[i]); }  // for-of & Object.values() for (const i of Object.values(person)) {   log(i); }  // Object.keys() & forEach() Object.keys(person).forEach((i) => log(person[i]));  // Object.values() & forEach() Object.values(person).forEach((i) => log(i));  // Object.entries() & forEach() Object.entries(person).forEach((i) => log(i[1])); 

Видите разницу? Приходится прибегать к дополнительным ухищрениям, которые заключаются в преобразовании объекта в массив тем или иным способом, потому что:

  for (const value of person) {     log(value); // TypeError: person is not iterable   } 

О чем нам говорит это исключение? Оно говорит о том, что объект «person», впрочем, как и любой другой объект, не является итерируемым или, как еще говорят, итерируемой (перебираемой) сущностью.

О том, что такое перебираемые сущности и итераторы, очень хорошо написано в этом разделе Современного учебника по JavaScript. С вашего позволения, я не буду заниматься копипастой. Однако, настоятельно рекомендую потратить 20 минут на его прочтение. В противном случае, дальнейшее изложение не будет иметь для вас особого смысла.

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

Вот пример, приводимый Ильей Кантором:

// имеется такой объект const range = {   from: 1,   to: 5, };  // добавляем ему свойство Symbol.iterator range[Symbol.iterator] = function () {   return {     // текущее значение     current: this.from,     // последнее значение     last: this.to,      // обязательный для итератора метод     next() {       // если текущее значение меньше последнего       if (this.current <= this.last) {         // возвращаем такой объект, увеличивая значение текущего значения         return { done: false, value: this.current++ };       } else {         // иначе сообщаем о том, что значений для перебора больше нет         return { done: true };       }     },   }; };  for (const num of range) log(num); // 1 2 3 4 5 // работает! 

По сути, приведенный пример — это генератор, созданный с помощью итератора. Но вернемся к нашему объекту. Функция для превращения обычного объекта в итерируемый может выглядеть следующим образом:

const makeIterator = (obj) => {   // добавляем неперечисляемое свойство "size", аналогичное свойству "length" массива   Object.defineProperty(obj, "size", {     value: Object.keys(obj).length,   });    obj[Symbol.iterator] = (     i = 0,     values = Object.values(obj)   ) => ({     next: () => (       i < obj.size         ? { done: false, value: values[i++] }         : { done: true }     ),   }); }; 

Проверяем:

makeIterator(person);  for (const value of person) {   log(value); // John 30 developer } 

Получилось! Теперь мы легко можем преобразовать такой объект в массив, а также получить количество его элементов через свойство «size»:

const arr = Array.from(person);  log(arr); // ["John", 30, "developer"]  log(arr.size); // 3 

Мы можем упростить код нашей функции, если вместо итератора воспользуемся генератором:

const makeGenerator = (obj) => {   // другое неперечисляемое свойство   // возвращающее логическое значение   Object.defineProperty(obj, "isAdult", {     value: obj["age"] > 18,   });    obj[Symbol.iterator] = function* () {     for (const i in this) {       yield this[i];     }   }; };  makeGenerator(person);  for (const value of person) {   log(value); // John 30 developer }  const arr = [...person];  log(arr); // ["John", 30, "developer"]  log(person.isAdult); // true 

Можем ли мы использовать метод «next» сразу после создания итерируемого объекта?

log(person.next().value); // TypeError: person.next is not a function 

Для того, чтобы у нас появилась такая возможность, необходимо сначала вызвать Symbol.iterator объекта:

const iterablePerson = person[Symbol.iterator]();  log(iterablePerson.next()); // { value: "John", done: false } log(iterablePerson.next().value); // 30 log(iterablePerson.next().value); // developer log(iterablePerson.next().done); // true 

Стоит отметить, что при наобходимости создания итерируемого объекта, лучше сразу определить в нем Symbol.iterator. На примере нашего объекта:

const person = {   name: "John",   age: 30,   job: "developer",    [Symbol.iterator]: function* () {     for (const i in this) {       yield this[i];     }   }, }; 

Двигаемся дальше. Куда дальше? В метапрограммирование. Что если мы хотим получать значения свойств объекта по индексу, как в массивах? И что если мы хотим, чтобы определенные свойства объекта были иммутабельными. Реализуем это поведение с помощью прокси. Почему с помощью прокси? Ну, хотя бы потому, что можем:

const makeProxy = (obj, values = Object.values(obj)) =>   new Proxy(obj, {     get(target, key) {       // преобразуем ключ в целое число       key = parseInt(key, 10);       // если ключ является числом, если он больше или равен 0 и меньше длины объекта       if (key !== NaN && key >= 0 && key < target.size) {         // возвращаем соответствующее свойство         return values[key];       } else {         // иначе сообщаем, что такого свойства нет         throw new Error("no such property");       }     },     set(target, prop, value) {       // при попытке перезаписать свойство "name" или свойство "age"       if (prop === "name" || prop === "age") {         // выбрасываем исключение         throw new Error(`this property can't be changed`);       } else {         // иначе добавляем свойство в объект         target[prop] = value;         return true;       }     },   });  const proxyPerson = makeProxy(person); // получаем свойство log(proxyPerson[0]); // John // пытаемся получить несуществующее свойство log(proxyPerson[2]); // Error: no such property // добавляем новое свойство log((proxyPerson[2] = "coding")); // true // пытаемся перезаписать иммутабельное свойство log((proxyPerson.name = "Bob")); // Error: this property can't be changed 

Какие выводы мы можем сделать из всего этого? Создать итерируемый объект своими силами, конечно, можно (это JavaScript, детка), но вопрос в том, зачем это делать. Следует согласиться с Руководством от Airbnb в том, что нативных методов более чем достаточно для решения всего спектра задач, связанных с перебором ключей и значений объектов, нет необходимости «изобретать велосипед». Руководство же от Google можно уточнить тем, что цикл for-of следует предпочитать для массивов и массивов из объектов, для объектов же как таковых можно использовать цикл for-in, но лучше — встроенные функции.

Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание.

ссылка на оригинал статьи https://habr.com/ru/post/526436/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *