Una nota sobre iterables





¡Buen dia amigos!



Esta nota no tiene ningún valor práctico particular. Por otro lado, explora algunas de las características "límite" de JavaScript que pueden resultarle interesantes.



La Guía de estilo de JavaScript de Goggle le aconseja priorizar for-of siempre que sea posible.



La Guía de estilo JavaScript de Airbnb desaconseja el uso de iteradores. En lugar de bucles for-in y for-of, debe usar funciones de orden superior como map (), every (), filter (), find (), findIndex (), reduce (), some () para iterar sobre matrices y Object .keys (), Object.values ​​(), Object.entries () para iterar sobre matrices de objetos. Más sobre eso más tarde.



Volvamos a Google. ¿Qué significa "donde sea posible"?



Veamos un par de ejemplos.



Digamos que tenemos una matriz como esta:



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


Y queremos enviar los valores de sus elementos a la consola. Cómo hacemos esto?



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


Todo funciona muy bien sin ningún esfuerzo adicional de nuestra parte.



Ahora, suponga que tenemos un objeto como este:



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


Y queremos hacer lo mismo.



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


¿Ver la diferencia? Tenemos que recurrir a trucos adicionales, que consisten en convertir un objeto en un arreglo de una forma u otra, porque:



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


¿Qué nos dice esta excepción? Dice que el objeto "persona", sin embargo, como cualquier otro objeto, no es una entidad iterable o, como dicen, una entidad iterable (iterable).



Acerca de qué iterables e iteradores están muy bien escritos en esta sección del Tutorial de JavaScript moderno. Con su permiso, no copiaré ni pegaré. Sin embargo, recomiendo encarecidamente dedicar 20 minutos a leerlo. De lo contrario, la presentación adicional no tendrá mucho sentido para usted.



Digamos que no nos gusta que los objetos no sean iterables y queremos cambiar eso. Cómo hacemos esto?



Aquí hay un ejemplo dado por 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
// !


Básicamente, el ejemplo proporcionado es un generador creado con un iterador. Pero volvamos a nuestro objetivo. Una función para convertir un objeto regular en un iterable podría verse así:



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


Verificamos:



makeIterator(person);

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


¡Ocurrió! Ahora podemos convertir fácilmente un objeto de este tipo en una matriz, así como obtener el número de sus elementos a través de la propiedad "tamaño":



const arr = Array.from(person);

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

log(arr.size); // 3


Podemos simplificar el código de nuestra función usando un generador en lugar de un iterador:



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


¿Podemos usar el método "siguiente" inmediatamente después de crear el iterable?



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


Para que tengamos esta oportunidad, primero debemos llamar al Symbol.iterator del objeto:



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


Vale la pena señalar que si necesita crear un objeto iterable, es mejor definir inmediatamente Symbol.iterator en él. Usando nuestro objeto como ejemplo:



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

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


Hacia adelante. ¿Dónde ir? En metaprogramación. ¿Qué pasa si queremos obtener los valores de las propiedades del objeto por índice, como en matrices? ¿Y si queremos que ciertas propiedades de un objeto sean inmutables? Implementemos este comportamiento usando un proxy . ¿Por qué usar un proxy? Bueno, aunque solo sea porque podemos:



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


¿Qué conclusiones podemos sacar de todo esto? Puede, por supuesto, crear un objeto iterable por su cuenta (es JavaScript, bebé), pero la pregunta es por qué. Estamos de acuerdo con la Guía de Airbnb en que existen métodos nativos más que suficientes para resolver toda la gama de tareas relacionadas con la iteración sobre claves y valores de objetos, no hay necesidad de “reinventar la rueda”. La guía de Google se puede aclarar por el hecho de que el bucle for-of debe preferirse para matrices y matrices de objetos, para objetos como tales, puede usar el bucle for-in, pero mejor, funciones integradas.



Espero que hayas encontrado algo interesante para ti. Gracias por su atención.



All Articles