Une note sur les itérables





Bonne journée, mes amis!



Cette note n'a pas de valeur pratique particulière. D'autre part, il explore certaines des fonctionnalités «limites» de JavaScript que vous pourriez trouver intéressantes.



Le guide de style JavaScript de Goggle vous conseille de prioriser autant que possible.



Le JavaScript Style Guide d'Airbnb déconseille l'utilisation d'itérateurs. Au lieu de boucles for-in et for-of, vous devriez utiliser des fonctions d'ordre supérieur comme map (), every (), filter (), find (), findIndex (), reduction (), some () pour itérer sur les tableaux et Object .keys (), Object.values ​​(), Object.entries () pour parcourir des tableaux d'objets. Plus à ce sujet plus tard.



Revenons à Google. Que signifie «là où c'est possible»?



Regardons quelques exemples.



Disons que nous avons un tableau comme celui-ci:



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


Et nous voulons afficher les valeurs de ses éléments dans la console. Comment faisons-nous cela?



//  
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));


Tout fonctionne parfaitement sans aucun effort supplémentaire de notre part.



Maintenant, supposons que nous ayons un objet comme celui-ci:



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


Et nous voulons faire de même.



// 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]));


Regarde la différence? Nous devons recourir à des astuces supplémentaires, qui consistent à convertir un objet en tableau d'une manière ou d'une autre, car:



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


Que nous dit cette exception? Il dit que l'objet "personne", cependant, comme tout autre objet, n'est pas une entité itérable ou, comme on dit, une entité itérable (itérable).



À propos des itérables et des itérateurs sont très bien écrits dans cette section du tutoriel JavaScript moderne. Avec votre permission, je ne vais pas copier-coller. Cependant, je recommande fortement de passer 20 minutes à le lire. Sinon, la présentation ultérieure n'aura pas beaucoup de sens pour vous.



Disons que nous n'aimons pas que les objets ne soient pas itérables, et nous voulons changer cela. Comment faisons-nous cela?



Voici un exemple donné par Ilya Kantor:



//   
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
// !


Fondamentalement, l'exemple fourni est un générateur créé avec un itérateur. Mais revenons à notre objet. Une fonction pour transformer un objet normal en un itérable pourrait ressembler à ceci:



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 }
    ),
  });
};


Nous vérifions:



makeIterator(person);

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


Arrivé! Maintenant, nous pouvons facilement convertir un tel objet en un tableau, ainsi qu'obtenir le nombre de ses éléments via la propriété "size":



const arr = Array.from(person);

log(arr); // ["John", 30, "developer"]

log(arr.size); // 3


Nous pouvons simplifier notre code de fonction en utilisant un générateur au lieu d'un itérateur:



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


Pouvons-nous utiliser la méthode "next" immédiatement après avoir créé l'itérable?



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


Pour que nous ayons une telle opportunité, nous devons d'abord appeler le Symbol.iterator de l'objet:



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


Il est à noter que si vous avez besoin de créer un objet itérable, il est préférable de définir immédiatement Symbol.iterator dedans. En utilisant notre objet comme exemple:



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

  [Symbol.iterator]: function* () {
    for (const i in this) {
      yield this[i];
    }
  },
};


Passer à autre chose. Où aller? En métaprogrammation. Que faire si nous voulons obtenir les valeurs des propriétés des objets par index, comme dans les tableaux? Et si nous voulons que certaines propriétés d'un objet soient immuables. Implémentons ce comportement à l'aide d'un proxy . Pourquoi utiliser un proxy? Eh bien, ne serait-ce que parce que nous pouvons:



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


Quelles conclusions pouvons-nous tirer de tout cela? Vous pouvez, bien sûr, créer vous-même un objet itérable (c'est du JavaScript, bébé), mais la question est de savoir pourquoi. Nous convenons avec le Guide Airbnb qu'il existe plus qu'assez de méthodes natives pour résoudre toute la gamme des tâches liées à l'itération sur les clés et les valeurs des objets, il n'est pas nécessaire de «réinventer la roue». Le guide de Google peut être clarifié par le fait que la boucle for-of doit être préférée pour les tableaux et les tableaux d'objets, pour les objets en tant que tels, vous pouvez utiliser la boucle for-in, mais mieux - les fonctions intégrées.



J'espère que vous avez trouvé quelque chose d'intéressant pour vous-même. Merci de votre attention.



All Articles