Símbolos JavaScript, Iteradores, Geradores, Assinantes Async / Await e Assíncrono – Todos Explicados Simplesmente

Alguns recursos JavaScript (ECMAScript) são mais fáceis de entender do que outros. Generators parecem estranhos – como ponteiros em C / C ++. Symbols conseguem se parecer com primitivos e objetos ao mesmo tempo.

Esses recursos são todos inter-relacionados e construídos uns sobre os outros. Então você não pode entender uma coisa sem entender o outro.

Portanto, neste artigo, abordarei symbols , symbols global symbols , iterators , iterables , generators , async iterators async/await e async iterators . Vou explicar " por que " eles estão lá em primeiro lugar e também mostrar como eles funcionam com alguns exemplos úteis.

Este é um assunto relativamente avançado, mas não é ciência de foguetes. Este artigo deve lhe dar uma compreensão muito boa de todos esses conceitos.

OK, vamos começar.?

Símbolos

No ES2015, um novo (6º) tipo de dados chamado symbol foi criado.

PORQUE?

As três principais razões foram:

Razão # 1 – Adicionar novos recursos principais com compatibilidade com versões anteriores

Os desenvolvedores de JavaScript e o comitê ECMAScript ( TC39 ) precisavam de uma maneira de adicionar novas propriedades de objeto sem quebrar os métodos existentes, como loops for in ou métodos JavaScript como Object.keys .

Por exemplo, se eu tiver um objeto, var myObject = {firstName:'raja', lastName:'rao'} e se eu executar Object.keys(myObject) ele retornará [firstName, lastName] .

Agora, se adicionarmos outra propriedade, diga newProperty a myObject , e se você executar Object.keys(myObject) ela ainda deve retornar valores antigos (isto é, de alguma forma, ignorar a nova newproperty recém-adicionada) e mostrar apenas [firstName, lastName] – e não [firstName, lastName, newProperty] . Como fazer isso?

Nós não poderíamos fazer isso antes, então um novo tipo de dados chamado Symbols foi criado.

Se você adicionar newProperty como um símbolo, então Object.keys(myObject) ignoraria isso (como ele não sabe), e ainda retornará [firstName, lastName] !

Razão # 2 – Evite colisões de nomes

Eles também queriam manter essas propriedades únicas. Dessa forma, eles podem continuar adicionando novas propriedades (e você pode adicionar propriedades de objetos) ao global sem se preocupar com colisões de nomes.

Por exemplo, digamos que você tenha um objeto no qual esteja adicionando um custom toUpperCase ao Array.prototype global.

Agora, imagine que você carregou outra biblioteca (ou o ES2019 foi lançado) e tinha uma versão diferente do Array.prototype.toUpperCase. Então sua função pode quebrar por causa da colisão de nomes.

Então, como você resolve essa colisão de nomes que talvez não conheça? É aí que entram os Symbols . Eles criam internamente valores exclusivos que permitem criar propriedades de adição sem se preocupar com a colisão de nomes.

Razão # 3 – Ativar ganchos para os métodos centrais através de símbolos "conhecidos"

Suponha que você queira alguma função principal, digamos String.prototype.search para chamar sua função personalizada. Isto é, 'somestring'.search(myObject); deve chamar myObject's função de pesquisa myObject's e passar 'somestring' como um parâmetro! Como fazemos isso?

É aí que o ES2015 surgiu com um monte de símbolos globais chamados de símbolos "conhecidos". E desde que seu objeto tenha um desses símbolos como propriedade, você pode redirecionar as funções principais para chamar sua função!

Não podemos falar muito sobre isso agora, então vou abordar todos os detalhes um pouco mais adiante neste artigo. Mas primeiro, vamos aprender sobre como os símbolos realmente funcionam.

Criando Símbolos

Você pode criar um símbolo chamando uma função / objeto global chamado Symbol . Essa função retorna um valor do symbol de tipo de dados.

Nota: Os símbolos podem aparecer como Objetos porque eles possuem métodos, mas não são – eles são primitivos. Você pode pensar neles como objetos “especiais” que têm algumas semelhanças com objetos regulares, mas que não se comportam como objetos comuns.

Por exemplo: os símbolos têm métodos como os objetos, mas, ao contrário dos objetos, são imutáveis ??e únicos.

Símbolos não podem ser criados por palavras-chave "novas"

Como os símbolos não são objetos e a new palavra-chave deve retornar um Object, não podemos usar new para retornar um tipo de dados de symbols .

 var mySymbol = novo símbolo (); // lança erro 

Símbolos têm "descrição"

Símbolos podem ter uma descrição – é apenas para fins de registro.

 // minha variável mySymbol agora contém um valor único "símbolo" 
// sua descrição é "algum texto"
const mySymbol = Symbol ('algum texto');

Símbolos são únicos

 const mySymbol1 = Symbol ('algum texto'); 
const mySymbol2 = Symbol ('algum texto');
mySymbol1 == mySymbol2 // false

Símbolos se comportam como um singleton se usarmos o método "Symbol.for"

Em vez de criar um symbol via Symbol() , você pode criá-lo via Symbol.for(<key>) . Isso leva uma “chave” (string) para criar um símbolo. E se um símbolo com essa key já existir, ele simplesmente retorna o símbolo antigo! Então, ele se comporta como um singleton se usarmos o método Symbol.for .

 var mySymbol1 = Símbolo .para ( 'alguma tecla'); // cria um novo símbolo 
var mySymbol2 = Símbolo .para ( 'alguma tecla'); // ** retorna o mesmo símbolo
mySymbol1 == mySymbol2 // verdadeiro

A verdadeira razão para usar o .for é criar um símbolo em um lugar e acessar o mesmo símbolo de algum outro lugar.

Cuidado: Symbol.for tornará o símbolo não exclusivo, no sentido de que você acabará substituindo os valores se as chaves forem iguais! Portanto, tente evitar isso, se possível!

"Descrição" do símbolo versus "chave"

Apenas para tornar as coisas mais claras, se você não usar o Symbol.for , os Símbolos serão exclusivos. No entanto, se você usá-lo, se sua key não for única, os símbolos retornados também não serão exclusivos.

Símbolos podem ser uma chave de propriedade de objeto

Esta é uma coisa muito original sobre Símbolos – e também mais confusa. Embora pareçam um objeto, são primitivos. E podemos anexar um símbolo a um objeto como uma chave de propriedade como uma String.

Na verdade, esta é uma das principais formas de usar símbolos – como propriedades de objetos!

Nota: As propriedades do objeto que são símbolos são conhecidas como "propriedades com chave".

Operador de suportes vs. operador de ponto

Você não pode usar um operador de ponto porque os operadores de ponto só funcionam nas propriedades da cadeia de caracteres, portanto, você deve usar um operador de colchetes.

3 principais razões para usar símbolos – uma revisão

Vamos revisitar as três principais razões agora que sabemos como os símbolos funcionam.

Razão # 1 – Os símbolos são invisíveis para loops e outros métodos

O loop for-in no exemplo abaixo faz um loop sobre um objeto obj mas ele não sabe (ou ignora) prop3 e prop4 porque eles são símbolos.

Abaixo está outro exemplo onde Object.keys e Object.getOwnPropertyNames estão ignorando nomes de propriedades que são símbolos.

Razão # 2 – Símbolos são únicos

Suponha que você queira um recurso chamado Array.prototype.includes no objeto Array global. Ele irá colidir com o padrão includes método que JavaScript (ES2018) vem com out-of-the-box. Como você adiciona sem colidir?

Primeiro, crie uma variável com o nome próprio includes e atribua um símbolo a ela. Em seguida, adicione essa variável (agora um símbolo) ao Array global usando a notação de colchetes. Atribuir qualquer função que você deseja.

Por fim, chame essa função usando a notação de colchetes. Mas note que você deve passar o símbolo atual entre parênteses como: arr[includes]() e não como string.

Razão # 3. Símbolos bem conhecidos (isto é, símbolos "globais")

Por padrão, o JavaScript cria automaticamente um grupo de variáveis ??de símbolo e as atribui ao objeto global Symbol (sim, o mesmo Symbol() que usamos para criar símbolos).

No ECMAScript 2015, esses símbolos são adicionados aos métodos principais, como String.prototype.search e String.prototype.replace dos objetos principais, como matrizes e cadeias de caracteres.

Alguns exemplos desses símbolos são: Symbol.match , Symbol.replace , Symbol.search , Symbol.iterator e Symbol.split .

Como esses símbolos globais são globais e expostos, podemos fazer com que os métodos centrais chamem nossas funções personalizadas, em vez de internas.

Um exemplo: Symbol.search

Por exemplo, o método público String.prototype.search do objeto String pesquisa um regExp ou uma string e retorna o índice, se encontrado.

No ES2015, ele primeiro verifica se o método Symbol.search é implementado na consulta regExp (objeto RegExp). Se assim for, então ele chama essa função e delega o trabalho para isso. E objetos centrais como o RegExp implementam o símbolo Symbol.search que realmente faz o trabalho.

Funcionamento interno do Symbol.search (COMPORTAMENTO PADRÃO)

  1. 'rajarao'.search('rao');
  2. Converta "rajarao" em String String new String(“rajarao”) objeto new String(“rajarao”)
  3. Converta "rao" no objeto RegExp new Regexp(“rao”)
  4. Chame o método de search do objeto String “rajarao”.
  5. search método de search chama Symbol.search método Symbol.search no objeto “rao” (delega a busca de volta ao objeto “rao”) e passa o “rajarao”. Algo parecido com isto: "rao"[Symbol.search]("rajarao")
  6. "rao"[Symbol.search]("rajarao") retorna o resultado do índice como 4 para a função de search e, finalmente, a search retorna 4 volta ao nosso código.

O trecho de pseudo-código abaixo mostra como o código funciona internamente:

Mas a beleza é que, você não precisa mais passar pelo RegExp. Você pode passar qualquer objeto personalizado que implemente o Symbol.search e retornar o que quiser, e isso continuará funcionando.

Vamos dar uma olhada.

Personalizando o método String.search para chamar nossa função

O exemplo abaixo mostra como podemos fazer o String.prototype.search chamar a função de pesquisa da nossa classe Product – graças ao Symbol.search global Symbol .

Funcionamento interno do Symbol.search (COMPORTAMENTO PERSONALIZADO)

  1. 'barsoap'.search(soapObj);
  2. Converta "barsoap" em String String new String(“barsoap”) objeto new String(“barsoap”)
  3. Como soapObj já é um objeto, não faça nenhuma conversão
  4. Chame o método de search do objeto String "barsoap".
  5. search método de search chama Symbol.search método Symbol.search no objeto “ soapObj ” (ou seja, ele delega a busca de volta ao objeto “ soapObj ”) e passa o “barsoap”. Algo parecido com isto: soapObj[Symbol.search]("barsoap")
  6. soapObj[Symbol.search]("barsoap") retorna o resultado do índice como FOUND para search função e, finalmente, search retornos FOUND volta ao nosso código.

Espero que você tenha uma boa compreensão dos símbolos agora.

OK, vamos passar para os Iteradores.

Iteradores e Iterables

PORQUE?

Em quase todos os nossos aplicativos, estamos constantemente lidando com listas de dados e precisamos exibir esses dados no navegador ou no aplicativo para dispositivos móveis. Normalmente, escrevemos nossos próprios métodos para armazenar e extrair esses dados.

Mas o problema é que já temos métodos padrão como o laço for-of e o operador spread ( ) para extrair coleções de dados de objetos padrão, como matrizes, seqüências de caracteres e mapas. Por que não podemos usar esses métodos padrão para o nosso objeto também?

No exemplo abaixo, não podemos usar um loop for-of ou operador de spread para extrair dados de nossa classe Users . Temos que usar um método get personalizado.

Mas, não seria bom poder usar esses métodos existentes em nossos próprios objetos? Para conseguir isso, precisamos ter regras que todos os desenvolvedores possam seguir e fazer com que seus objetos funcionem com os métodos existentes.

Se seguirem essas regras para extrair dados de seus objetos, esses objetos serão chamados de "iteráveis".

As regras são:

  1. O objeto principal / classe deve armazenar alguns dados.
  2. O objeto / classe principal deve ter o symbol.iterator símbolo “conhecido” symbol.iterator como sua propriedade que implementa um método específico conforme as regras de 3 a 6.
  3. Este método symbol.iterator deve retornar outro objeto – um objeto "iterador".
  4. Esse objeto "iterador" deve ter um método chamado next method.
  5. O next método deve ter acesso aos dados armazenados na regra # 1.
  6. E se chamarmos iteratorObj.next() , ele deverá retornar alguns dados armazenados da regra nº 1 como formato {value:<stored data>, done: false } se desejar retornar mais valores ou como {done: true } se não quiser retornar mais dados.

Se todas essas 6 regras forem seguidas, então o objeto principal é chamado como um " iterável " da regra # 1. O objeto retornado é chamado de " iterador ".

Vamos dar uma olhada em como podemos tornar nossos Users objeto e iterável:

Clique para ampliar

Nota importante: Se passar um iterable ( allUsers ) for-of loop ou operador spread, internamente eles chamam de <iterable>[Symbol.iterator]() para obter o iterator (como allUsersIterator ) e, em seguida, usar o iterador para extrair dados.

Então, de certa forma, todas essas regras estão lá para ter uma maneira padrão de retornar um objeto iterator .

Funções do gerador

PORQUE?

Há duas razões principais:

  1. fornecer abstração de alto nível para iteráveis
  2. Forneça fluxo de controle mais recente para ajudar com coisas como "callback-hell".

Vamos verificá-los detalhadamente.

RAZÃO # 1 – Um invólucro para iterables

Em vez de tornar a nossa classe / objeto um iterable , seguindo todas essas regras, podemos simplesmente criar algo chamado como um método "Generator" para simplificar as coisas.

Abaixo estão alguns dos principais pontos sobre Geradores:

  1. Os métodos do gerador têm uma nova sintaxe *<myGenerator> dentro de uma classe, e as funções do Generator têm a function * myGenerator(){} sintaxe function * myGenerator(){} .
  2. Os geradores de chamadas myGenerator() retornam um objeto generator que também implementa o protocolo do iterator (regras), para que possamos usá-lo como um valor de retorno do iterator myGenerator() para uso.
  3. Os geradores usam uma instrução de yield especial para retornar dados.
  4. yield declarações de yield acompanham as chamadas anteriores e simplesmente continuam de onde pararam.
  5. Se você usar o yield dentro de um loop, ele só será executado uma vez a cada vez que chamarmos o método next() no iterador.

Exemplo 1:

O código abaixo mostra como você pode usar um método gerador ( *getIterator() ) em vez de usar o método Symbol.iterator e implementar o next método que segue todas as regras.

Usando geradores dentro de uma classe

Exemplo 2:

Você pode simplificá-lo ainda mais. Torne uma função um gerador (com * sintaxe) e use o yield para retornar valores um por vez, como mostrado abaixo.

Usando geradores diretamente como funções

Nota importante : Embora nos exemplos acima, eu esteja usando a palavra "iterador" para representar allUsers , é realmente um objeto generator .

O objeto gerador tem métodos como throw e return , além do next método! Mas para fins práticos, podemos usar o objeto retornado como apenas "iterador".

RAZÃO # 2 – Fornecer fluxos de controle melhores e mais recentes

Ajude a fornecer novos fluxos de controle que nos ajudem a escrever programas de novas maneiras e a resolver coisas como "inferno de retorno de chamada".

Observe que, diferentemente de uma função normal, a função geradora pode yield (armazenar o state e o valor de return da função) e também estar pronta para receber valores de entrada adicionais no ponto em que produziu.

Na figura abaixo, toda vez que vê yield , pode retornar o valor. Você pode usar generator.next(“some new value”) e passar o novo valor no ponto em que produziu.

Função normal vs função Generator

O exemplo abaixo mostra em termos mais concretos como funciona o fluxo de controle:

Fluxo de controle do gerador

Sintaxe e uso do gerador

As funções do gerador podem ser usadas das seguintes maneiras:

Podemos ter mais código depois de "yield" (diferente da instrução "return")

Assim como a palavra-chave return , a palavra-chave yield também retorna o valor – mas nos permite ter código após render!

Você pode ter vários rendimentos

você pode ter várias declarações de rendimento

Envio de valores para os geradores através do método "next"

O next método dos iteradores também pode passar os valores de volta ao gerador, como mostrado abaixo.

Na verdade, esse recurso permite que os geradores eliminem o "inferno de retorno de chamada". Você aprenderá mais sobre isso daqui a pouco.

Esse recurso também é usado intensamente em bibliotecas como redux-saga .

No exemplo abaixo, chamamos o iterador com uma chamada next() vazia para obter a pergunta. E então, passamos 23 como o valor quando chamamos o next(23) a segunda vez.

Valor de volta para o gerador de fora via "next"

Geradores ajudam a eliminar o "inferno callback"

Você sabe que entramos em callback hell se tivermos várias chamadas assíncronas.

O exemplo abaixo mostra como bibliotecas como “ co ” usam o recurso de gerador que nos permite passar um valor através do next método para nos ajudar a escrever código assíncrono de forma síncrona.

Observe como a função co passa o resultado da promessa de volta para o gerador via next(result) na etapa nº 5 e etapa nº 10.

Explicação passo-a-passo de bibliotecas como “co” que usam “next (<someval>)”

OK, vamos passar para async / await.

ASYNC / AWAIT

PORQUE?

Como você viu anteriormente, os Geradores podem ajudar a eliminar o “inferno callback”, mas você precisa de alguma biblioteca de terceiros como co para fazer isso acontecer. Mas “callback hell” é um problema tão grande, o comitê ECMAScript decidiu criar um wrapper apenas para esse aspecto do Generator e saiu com as novas palavras async/await chave async/await .

As diferenças entre Geradores e Async / Await são:

  1. async / await usa await vez de yield .
  2. await só funciona com promessas.
  3. Em vez de function* , ele usa a palavra-chave da async function .

Então async/await é essencialmente um subconjunto de Generators e tem um novo açúcar sintático.

A palavra async chave async diz ao compilador JavaScript para tratar a função de maneira diferente. O compilador faz uma pausa sempre que atinge a palavra-chave await dentro dessa função. Assume-se que a expressão após await retorna uma promessa e aguarda até que a promessa seja resolvida ou rejeitada antes de prosseguir.

No exemplo abaixo, a função getAmount está chamando duas funções assíncronas getUser e getBankBalance . Podemos fazer isso em uma promessa, mas o uso do async await é mais elegante e simples.

ITERATORES DE ASYNC

PORQUE?

É um cenário bem comum em que precisamos chamar funções assíncronas em um loop. Assim, no ES2018 (proposta finalizada), o comitê do TC39 apresentou um novo Symbol Symbol.asyncIterator e também um novo constructo for-await-of para nos ajudar facilmente a executar o loop de funções assíncronas.

A principal diferença entre os objetos Iterator regulares e os Iteradores Assíncronos é a seguinte:

Objeto Iterator

  1. O método next() objeto Iterator retorna valor como {value: 'some val', done: false}
  2. Uso: iterator.next() //{value: 'some val', done: false}

Objeto Iterator assíncrono

  1. O método next () do objeto Iterator assíncrono retorna um Promise que depois é resolvido em algo como {value: 'some val', done: false}
  2. Uso: iterator.next().then(({ value, done })=> {//{value: 'some val', done: false}}

O exemplo abaixo mostra como funciona for-await-of e como você pode usá-la.

à espera de (ES2018)

RESUMO

Símbolos – fornecem um tipo de dados globalmente exclusivo. Você os usa principalmente como propriedades de objetos para adicionar novos comportamentos, assim você não quebra os métodos padrão, como Object.keys e loops for-in .

Símbolos conhecidos – são símbolos gerados automaticamente por JavaScript e podem ser usados ??para implementar métodos centrais em nossos objetos personalizados

Iterables – são quaisquer objetos que armazenam uma coleção de dados e seguem regras específicas para que possamos usar os operadores standard for-of loop e ... spread para extrair dados de dentro deles.

Iteradores – são retornados por Iterables e têm o next método – é o que realmente extrai os dados de um iterable .

Geradores – fornecem abstração de nível superior para Iterables. Eles também fornecem novos fluxos de controle que podem resolver coisas como callback-hell e fornecer blocos de construção para coisas como Async/Await .

Async / Await – fornece abstração de nível superior para os Geradores, a fim de resolver especificamente o problema callback-hell.

Iteradores Assíncronos – um novíssimo recurso 2018 para ajudar no loop de uma série de funções assíncronas para obter o resultado de cada função assíncrona como em um loop normal.

Isso é muito bonito isso!

Leitura adicional

ECMAScript 2015+

  1. Aqui estão alguns exemplos de tudo novo no ECMAScript 2016, 2017 e 2018
  2. Confira essas dicas e truques úteis do ECMAScript 2015 (ES6)
  3. 5 partes "ruins" do JavaScript que são corrigidas no ES6
  4. É “Class” no ES6 A nova parte “ruim”?

Meus outros posts podem ser encontrados aqui .

Se isto foi útil, por favor clique no botão clap ? abaixo algumas vezes para mostrar o seu apoio! ???