Uma nota sobre iteráveis





Bom dia amigos!



Esta nota não tem nenhum valor prático particular. Por outro lado, ele explora alguns dos recursos "limítrofes" do JavaScript que você pode achar interessantes.



O Guia de estilo JavaScript do Goggle aconselha você a priorizar para-de sempre que possível.



O Guia de Estilo JavaScript do Airbnb desencoraja o uso de iteradores. Em vez de loops for-in e for-of, você deve usar funções de ordem superior, como map (), every (), filter (), find (), findIndex (), reduce (), some () para iterar em arrays e Object .keys (), Object.values ​​(), Object.entries () para iterar em matrizes de objetos. Mais sobre isso mais tarde.



Vamos voltar ao Google. O que significa “onde possível”?



Vejamos alguns exemplos.



Digamos que temos uma matriz como esta:



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


E queremos enviar os valores de seus elementos para o console. Como vamos fazer isso?



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


Tudo funciona muito bem sem nenhum esforço extra de nossa parte.



Agora, suponha que temos um objeto como este:



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


E queremos fazer o mesmo.



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


Veja a diferença? Temos que recorrer a truques adicionais, que consistem em converter um objeto em um array de uma forma ou de outra, porque:



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


O que essa exceção nos diz? Diz que o objeto "pessoa", entretanto, como qualquer outro objeto, não é uma entidade iterável ou, como dizem, uma entidade iterável (iterável).



Sobre quais iteráveis ​​e iteradores estão muito bem escritos nesta seção do Tutorial de JavaScript moderno. Com sua permissão, não vou copiar e colar. No entanto, eu recomendo fortemente que você gaste 20 minutos lendo-o. Caso contrário, uma discussão mais aprofundada não fará muito sentido para você.



Digamos que não gostamos do fato de os objetos não serem iteráveis ​​e queremos mudar isso. Como vamos fazer isso?



Aqui está um exemplo 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
// !


Essencialmente, o exemplo fornecido é um gerador criado com um iterador. Mas voltando ao nosso objetivo. Uma função para transformar um objeto regular em um iterável pode ter a seguinte aparência:



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


Nós verificamos:



makeIterator(person);

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


Aconteceu! Agora podemos converter facilmente esse objeto em uma matriz e também obter o número de seus elementos por meio da propriedade "size":



const arr = Array.from(person);

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

log(arr.size); // 3


Podemos simplificar nosso código de função usando um gerador em vez de um 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 o método "próximo" imediatamente após criar o iterável?



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


Para termos essa oportunidade, devemos primeiro chamar o Symbol.iterator do 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


É importante notar que, se você precisa criar um objeto iterável, é melhor definir imediatamente Symbol.iterator nele. Usando nosso objeto como exemplo:



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

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


Se movendo. Onde ir? Em metaprogramação. E se quisermos obter os valores das propriedades do objeto por índice, como em arrays? E se quisermos que certas propriedades de um objeto sejam imutáveis. Vamos implementar esse comportamento usando um proxy . Por que usar um proxy? Bem, apenas 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


Que conclusões podemos tirar de tudo isso? Você pode, é claro, criar um objeto iterável por conta própria (é JavaScript, baby), mas a questão é por quê. Concordamos com o Guia do Airbnb que existem métodos nativos mais do que suficientes para resolver toda a gama de tarefas relacionadas à iteração de chaves e valores de objetos, não há necessidade de “reinventar a roda”. O guia do Google pode ser esclarecido pelo fato de que o loop for-of deve ser preferido para arrays e arrays de objetos, para objetos como tal, você pode usar o loop for-in, mas melhor - funções integradas.



Espero que você tenha encontrado algo interessante para você. Obrigado pela atenção.



All Articles