Fluxo de controle

O fluxo de controle é a ordem em que o intérprete de JavaScript executa instruções. Se um script não incluir instruções que alterem o fluxo, ele será executado do início ao fim, uma linha por vez. Estruturas de controle são usadas para determinar se um conjunto de instruções é executado com base em um conjunto definido de critérios, executar um conjunto de instruções repetidamente ou interromper uma sequência de instruções.

Instruções condicionais

As instruções condicionais determinam se o código precisa ser executado com base em uma ou mais condições. Uma instrução condicional vai executar o código que ela contém se a condição associada (ou conjunto de condições) for avaliada como true. Caso contrário, o código é ignorado.

ifelse

Uma instrução if avalia uma condição dentro dos parênteses correspondentes a seguir. Se a condição entre parênteses for avaliada como true, a instrução ou a instrução de bloco que segue os parênteses correspondentes será executada:

if ( true ) console.log( "True." );
> "True."

if ( true ) {
    const myString = "True.";
    console.log( myString );
}
> "True."

Se a condição entre parênteses for avaliada como false, a instrução a seguir será ignorada:

if ( false ) console.log( "True." );

Uma palavra-chave else imediatamente após uma instrução if e a instrução executada condicionalmente especifica a instrução a ser executada se a condição if for avaliada como false:

if ( false ) console.log( "True." )''
else console.log( "False" );
> "False."

Para encadear várias instruções if, é possível fazer a instrução executada condicionalmente seguindo else outra instrução if:

const myCondition = 2;
if ( myCondition === 5 ) console.log( "Five." );
else if ( myCondition === 2 ) console.log( "Two." );

É altamente recomendável usar a sintaxe de instrução de bloco após as condicionais para melhorar a legibilidade, mas as cláusulas else if costumam ser uma exceção:

if ( myCondition === 5 ) {
    console.log( "Five." );
} else if ( myCondition === 3 ) {
    console.log( "Three" );
} else {
    console.log( "Neither five nor three." );
}
> "Neither five nor three."

Operador ternário

if executa condicionalmente uma instrução. O operador ternário (de forma mais precisa, mas menos comumente chamado de operador condicional ternário) é uma abreviação usada para executar uma expressão condicionalmente. Como o nome indica, o operador ternário é o único operador JavaScript que usa três operandos:

  • Uma condição a ser avaliada, seguida por um ponto de interrogação (?).
  • A expressão a ser executada se a condição for avaliada como true, seguida por dois-pontos (:).
  • A expressão a ser executada se a condição for avaliada como false.

Isso é usado com frequência para definir ou transmitir condicionalmente um valor:

const myFirstResult  = true  ? "First value." : "Second value.";
const mySecondResult = false ? "First value." : "Second value.";

myFirstResult;
> "First value."

mySecondResult;
> "Second value."

switchcase

Use a instrução switch para comparar o valor de uma expressão com uma lista de valores em potencial definidos usando uma ou mais palavras-chave case. Essa sintaxe é incomum porque vem de algumas das primeiras decisões de design do JavaScript. A sintaxe switch...case usa a palavra-chave switch, seguida por uma expressão a ser avaliada entre parênteses e por um par correspondente de chaves. O corpo de switch pode conter palavras-chave case, geralmente uma ou mais, seguidas por uma expressão ou valor, seguidos por dois-pontos (:).

Quando o intérprete atinge um case com um valor correspondente à expressão que está sendo avaliada entre os parênteses após a palavra-chave switch, ele executa todas as instruções após essa cláusula case:

switch ( 2 + 2 === 4 ) {
  case false:
    console.log( "False." );
  case true:
    console.log( "True." );
}
> "True."

Todas as instruções após o case correspondente são executadas, mesmo que estejam incluídas em uma instrução de bloco.

switch ( 2 + 2 === 4 ) {
    case false:
    console.log( "False." );
  case true:
    let myVariable = "True.";
    console.log( myVariable );

}
> "True."

Um erro no uso de switch…case é que, depois que uma correspondência é encontrada, o intérprete de JavaScript executa qualquer instrução que siga a case correspondente, mesmo aquelas dentro de outras cláusulas case. Isso é chamado de "substituição" para a próxima case:

switch ( 2 + 2 === 7 ) {
    case false:
    console.log( "False." );
  case true:
    console.log( "True." );
}
> "False."
> "True."

Para evitar falhas, encerre cada caso com a palavra-chave break, que interrompe imediatamente a avaliação do corpo switch:

switch ( 2 + 2 === 7 ) {
    case false:
    console.log( "False." );
    break;
  case true:
    console.log( "True." );
    break;
}
> "False."

Se nenhum case corresponder ao valor condicional, o switch selecionará a cláusula default, se houver uma:

switch ( 20 ) {
    case 5:
    console.log( "The value was five." );
    break;
  case 10:
    console.log( "The value was ten." );
    break;
  default:
    console.log( "The value was something unexpected." );
}
> "The value was something unexpected."

No entanto, as falhas se aplicam a default também, possivelmente levando a resultados inesperados. Para corrigir isso, encerre a instrução default com break ou coloque-a por último na lista de casos.

switch ( 20 ) {
  default:
    console.log( "The value was something unexpected." );
  case 10:
    console.log( "The value was ten." );
    break;
  case 5:
    console.log( "The value was five." );
    break;
}
> The value was something unexpected.
> The value was ten.

Como as cláusulas case não exigem uma instrução de bloco para agrupar várias instruções, as cláusulas case e default não criam escopo léxico sozinhas:

let myVariable;
switch ( true ) {
  case true:
    let myVariable = "True.";
    break;
  default:
    let myVariable = "False.";
    break;
}
> Uncaught SyntaxError: redeclaration of let myVariable

Para gerenciar o escopo, use instruções de bloco:

let myVariable;
switch ( true ) {
  case true: {
    let myVariable = "True.";
    break;
  }
  default: {
    let myVariable = "False.";
    break;
  }
}

Loops e iteração

Os loops permitem que você repita um conjunto de instruções enquanto uma condição for atendida ou até que uma condição seja atendida. Use loops para executar um conjunto de instruções um número fixo de vezes, até que um resultado específico seja alcançado ou até o intérprete chegar ao fim de uma estrutura de dados iterável (por exemplo, o elemento final em uma matriz, mapa ou conjunto, a propriedade final de um objeto ou o último caractere em uma string).

Os loops interrompem o fluxo "de cima para baixo" da execução de um script iterando um conjunto de instruções até que uma ou mais condições sejam atendidas ou deixem de ser atendidas, dependendo da sintaxe usada para criar a repetição. Após o fim do loop, a execução segue para as instruções que o seguem. No exemplo a seguir, as instruções no corpo do loop são executadas três vezes antes de o intérprete avançar:

let iterationCount = 0;
console.log( "Pre-loop." );
while( iterationCount < 3 ) {
  iterationCount++;
  console.log( "Loop iteration." );
}
console.log( "Continuing on." );
> "Pre-loop."
> "Loop iteration."
> "Loop iteration."
> "Loop iteration."
> "Continuing on."

Se não for possível atender às condições durante a execução do loop, ele continuará indefinidamente. Esses loops infinitos são um problema comum de programação que pode fazer com que a linha de execução principal seja pausada indefinidamente ou até mesmo trave uma guia do navegador.

O exemplo a seguir é executado enquanto o valor booleano true permanecer como true. Como os valores booleanos são imutáveis, isso cria um loop infinito.

console.log( "Pre-loop." );
while( true ) {
console.log( "Loop iteration." );
}
> "Pre-loop."
> "Loop iteration."
> "Loop iteration."
> "Loop iteration."
> "Loop iteration."
> "Loop iteration."
…

Evite deixar loops infinitos no seu código de produção. Se você criar uma sem querer durante o desenvolvimento, poderá corrigi-la fechando a guia do navegador em que ela está sendo executada, atualizando seu código para que a repetição não seja mais infinita e reabrindo a página.

while

Uma repetição while é criada usando a palavra-chave while seguida por um par de parênteses correspondentes contendo uma condição a ser avaliada. Se a condição especificada for avaliada inicialmente como true, a instrução (ou instrução de bloco) que segue esses parênteses será executada. Caso contrário, a repetição nunca será executada. Após cada iteração, a condição é reavaliada e, se ainda for true, o loop será repetido.

let iterationCount = 0;
while( iterationCount < 3 ) {
  iterationCount++;
  console.log( `Loop ${ iterationCount }.` );
}
> "Loop 1."
> "Loop 2."

Se o intérprete encontrar uma instrução continue em um loop while, ele interromperá essa iteração, reavaliará a condição e continuará o loop, se possível:

let iterationCount = 0;
while( iterationCount <= 5 ) {
  iterationCount++;
  if( iterationCount === 3 ) {
    continue;
  }
  console.log( `Loop ${ iterationCount }.` );
}
console.log( "Loop ended." );
> "Loop 1."
> "Loop 2."
> "Loop 4."
> "Loop 5."
> "Loop ended."

Se o intérprete encontrar uma instrução break em uma repetição while, essa iteração será interrompida e a condição não será reavaliada, permitindo que o intérprete prossiga:

let iterationCount = 1;
while( iterationCount <= 5 ) {
  if( iterationCount === 3 ) {
    console.log(`Iteration skipped.``);`
    break;
  }
  console.log( `Loop ${ iterationCount }.` );
  iterationCount++;
}
console.log( "Loop ended." );
> "Loop 1."
> "Loop 2."
> "Iteration skipped.
> "Loop ended."

É possível usar while para iterar um número especificado de vezes, como visto no exemplo anterior, mas o caso de uso mais comum para while é uma repetição de tamanho indeterminado:

let randomize = () => Math.floor( Math.random() * 10 );
let randomNum = randomize();
while( randomNum !== 3 ){
  console.log( `The number is not ${ randomNum }.` );
  randomNum = randomize();
}
console.log( `The correct number, ${ randomNum }, was found.` );
> "The number is not 0."
> "The number is not 6."
> "The number is not 1."
> "The number is not 8."
> "The correct number, 3, was found."

dowhile

do...while é uma variante da repetição while, em que a avaliação condicional acontece no fim de cada iteração do loop. Isso significa que o corpo do loop é sempre executado pelo menos uma vez.

Para criar uma repetição do...while, use a palavra-chave do seguida pela instrução (ou instrução de bloco) que será executada em cada iteração da repetição. Imediatamente após essa instrução, adicione while e parênteses correspondentes contendo a condição a ser avaliada. Quando essa condição não for mais avaliada como true, o loop terminará.

let iterationCount = 1;
do {
  console.log( `Loop ${ iterationCount }.` );
  iterationCount++;
} while ( iterationCount < 3 );
> "Loop 1."
> "Loop 2."
> "Loop 3."

Assim como acontece com uma repetição while, o caso de uso mais comum para do...while é uma repetição de tamanho indeterminado:

let randomNum;
do {
  randomNum = ( () => Math.floor( Math.random() * 10 ) )();
  console.log( `Is the number ${ randomNum }?` );
} while ( randomNum !== 3 );
console.log( `Yes, ${ randomNum } was the correct number.` );
> "Is the number 9?"
> "Is the number 2?"
> "Is the number 8?"
> "Is the number 2?"
> "Is the number 3?"
> "Yes, 3 was the correct number."

for

Use repetições for para iterar uma quantidade conhecida. Em bases de código legadas, isso era usado com frequência para iterar os elementos de uma matriz.

Para criar uma repetição for, use a palavra-chave for, seguida de um conjunto de parênteses que aceita as três expressões a seguir em ordem e separadas por ponto e vírgula:

  1. Expressão a ser avaliada quando o loop começa.
  2. Condição que determina se o loop deve continuar.
  3. Uma expressão a ser executada na conclusão de cada loop

Depois dos parênteses, adicione a instrução (normalmente uma instrução de bloco) a ser executada durante o loop.

for( let i = 0; i < 3; i++ ) {
  console.log( "This loop will run three times.")
}

A primeira expressão inicializa uma variável que atua como um contador. Essa expressão é avaliada uma vez, antes da primeira iteração do loop. Você pode inicializar essa variável usando let (ou var, historicamente) como qualquer outra variável, e o escopo é o corpo do loop. Essas variáveis podem ter qualquer identificador válido, mas são frequentemente chamadas de i para "iteração" ou "índice". Isso parece contrariar as práticas recomendadas para nomes de identificadores previsíveis, mas a convenção está bem estabelecida o suficiente para ser clara para outros desenvolvedores rapidamente. Como as coleções indexadas são indexadas a zero, essas variáveis quase sempre têm um valor inicial de 0.

Como acontece com outras formas de loop, a condição é uma expressão que determina se o loop será executado. Ele é usado com mais frequência para definir um limite superior para o contador de iteração. O intérprete avalia a condição antes de executar a repetição for pela primeira vez.Se a condição não for avaliada inicialmente como true, o corpo do loop não será executado.

A expressão final é executada no final de cada iteração pelo loop. Normalmente, ele é usado para incrementar o identificador em um.

Você vai encontrar com mais frequência repetições de for iterando por matrizes em bases de código mais antigas. Nesses casos, a condição especificada para continuar o loop é uma contagem de iterações menor ou igual ao comprimento da matriz que está sendo iterada. A variável usada para rastrear a contagem de iterações atuais é usada para pesquisar o valor associado a esse índice na matriz, permitindo que cada elemento da matriz seja executado em ordem:

var myArray = [ true, false, true ];
for( let i = 0; i <= myArray.length; i++ ) {
  console.log( myArray[ i ] );
}
> true
> false
> true

Essa abordagem deixou de ser usada em favor de abordagens mais modernas para passar por estruturas de dados iteráveis.

for [...] of [...]

Use repetições for...of... para iterar os valores armazenados em uma estrutura de dados iterável, como uma matriz, um conjunto ou um mapa.

Uma repetição for...of... usa a palavra-chave for seguida por um conjunto de parênteses contendo uma variável, seguido por of e, em seguida, a estrutura de dados que está sendo iterada. A variável pode ser uma declaração realizada aqui usando let, const ou var, uma variável declarada anteriormente no escopo atual, uma propriedade de objeto ou uma instância de atribuição de desestruturação. Ele contém o valor do elemento que corresponde à iteração atual do loop.

const myIterable = [ true, false, true ];
for( const myElement of myIterable ) {
  console.log( myElement );
}
> true
> false
> true

Nesse exemplo, o uso de const para myElement funciona mesmo que myElement receba um novo valor em cada iteração do loop. Isso ocorre porque as variáveis declaradas com let ou const têm o escopo definido para a instrução de bloco no loop. A variável é inicializada no início de cada iteração e removida no final.

forin

Use repetições for...in... para iterar as propriedades enumeráveis de um objeto, incluindo propriedades herdadas enumeráveis. Assim como acontece com uma repetição for...of..., uma repetição for...in... usa a palavra-chave for seguida por um conjunto de parênteses que contém uma variável que contém o valor da chave de propriedade correspondente à iteração atual da repetição. Essa variável é seguida pela palavra-chave in e, em seguida, pelo objeto que está sendo iterado:

const myObject = { "myProperty" : true, "mySecondProperty" : false };
for( const myKey in myObject ) {
  console.log( myKey );
}
> "myProperty"
> "mySecondProperty"

Novamente, apesar de o valor de myKey mudar a cada iteração do loop, é possível usar const sem erros, porque a variável é efetivamente descartada no final de cada iteração e recriada no início.

O valor associado a cada chave de propriedade não está diretamente disponível para a sintaxe for...in. No entanto, como o loop tem acesso a uma chave de propriedade em cada iteração, é possível usar essa chave para "procurar" o valor:

const myObject = { "myProperty" : true, "mySecondProperty" : false };
for( const myKey in myObject ) {
  const myValue = myObject[ myKey ];
  console.log( `${ myKey } : ${ myValue }` );
}
> "myProperty : true"
> "mySecondProperty : false"

As propriedades herdadas de construtores integrados não são enumeração, o que significa que for...in... não faz iterações nas propriedades herdadas do construtor Object. No entanto, todas as propriedades enumeráveis na cadeia de protótipo do objeto são incluídas:

const myPrototype = { "protoProperty" : true };
const myObject = Object.create( myPrototype, {
    myProperty: {
    value: true,
    enumerable: true
    }
});
for ( const myKey in myObject ) {
  const myValue = myObject[ myKey ];
  console.log( `${ myKey } : ${ myValue }` );
}
> "myProperty : true"
> "protoProperty : true"

O JavaScript fornece métodos integrados para determinar se uma propriedade é uma propriedade direta do objeto em vez de uma propriedade na cadeia de protótipo dele: os métodos Object.hasOwn() modernos e Object.prototype.hasOwnProperty() legados. Esses métodos avaliam se uma propriedade especificada é herdada (ou não declarada), retornando true apenas para as propriedades imediatas de um objeto especificado:

const myPrototype = { "protoProperty" : true };
const myObject = Object.create( myPrototype, {
    myProperty: {
    value: true,
    enumerable: true
    }
});
for ( const myKey in myObject ) {
  const myValue = myObject[ myKey ];
  if ( Object.hasOwn( myObject, myKey ) ) {
    console.log( `${ myKey } : ${ myValue }` );
  }
}
> "myProperty : true"

Há também três métodos estáticos em que cada um retorna uma matriz composta por chaves enumeráveis (Object.keys()), valores (Object.values()) ou pares de chave-valor (Object.entries()) de um objeto:

const myObject = { "myProperty" : true, "mySecondProperty" : false };
Object.keys( myObject );
> Array [ "myProperty", "mySecondProperty" ]

Isso permite iterar chaves, valores ou pares de chave-valor de objetos (usando a atribuição de desestruturação) sem incluir propriedades de propriedade do protótipo desse objeto:

const myPrototype = { "protoProperty" : "Non-enumerable property value." };
const myObject = Object.create( myPrototype, {
    myProperty: {
    value: "Enumerable property value.",
    enumerable: true
    }
});

for ( const propKey of Object.keys( myObject ) ) {
  console.log( propKey );
}
> "myProperty"

for ( const propValue of Object.values( myObject ) ) {
  console.log( propValue );
}
> "Enumerable property value."

for ( const [ propKey, propValue ] of Object.entries( myObject ) ) {
  console.log( `${ propKey } : ${ propValue }` );
}
> "myProperty : Enumerable property value."

forEach()

Os métodos forEach() fornecidos pelos construtores Array, Map, Set e NodeList oferecem uma abreviação útil para iterar uma estrutura de dados no contexto de uma função de callback. Ao contrário de outras formas de repetição, um loop criado com qualquer método forEach() não pode ser interrompido usando break ou continue.

forEach é um método que pertence ao protótipo de cada estrutura de dados. Cada método forEach espera uma função de callback como argumento, embora haja uma pequena variação em termos de argumentos incluídos quando essa função é chamada. Um segundo argumento opcional especifica um valor this a ser usado como contexto de invocação para a função de callback.

A função de callback usada com Array.forEach fornece parâmetros que contêm o valor do elemento atual, o índice do elemento atual e a matriz em que o método forEach foi invocado:

const myArray = [ true, false ];
myArray.forEach( ( myElement, i, originalArray ) => {
  console.log( i, myElement, originalArray  );
});
> 0 true Array(3) [ true, false ]
> 1 false Array(3) [ true, false ]

A função de callback usada com Map.forEach fornece parâmetros que contêm o valor associado ao elemento atual, a chave associada ao elemento atual e o mapa em que o método forEach foi invocado:

const myMap = new Map([
  ['myKey', true],
  ['mySecondKey', false ],
]);
myMap.forEach( ( myValue, myKey, originalMap ) => {
    console.log( myValue, myKey, originalMap  );
});
> true "myKey" Map { myKey → true, mySecondKey → false }
> false "mySecondKey" Map { myKey → true, mySecondKey → false }

Um callback Set.forEach inclui parâmetros semelhantes. Como Set não tem índices ou chaves diferentes de valores, o segundo argumento fornece um valor redundante e ignorável, estritamente para manter a sintaxe consistente com os outros métodos forEach.

const mySet = new Set([ true, false ]);
mySet.forEach( ( myValue, myKey, originalSet ) => {
  console.log( myValue, myKey, originalSet  );
});
> true true Set [ true, false ]
> false false Set [ true, false ]

Iteradores

Um iterável é qualquer estrutura de dados composta por elementos individuais que podem ser iterados usando as abordagens detalhadas anteriormente. Um iterador é um objeto iterável que segue o protocolo de iterador, o que significa que ele precisa implementar um método next() que avance pelos elementos contidos um por vez, sempre que esse método for chamado, retornando um objeto para cada elemento sequencial em um formato específico.

As estruturas de dados iteráveis e integradas do JavaScript (como Array, Map e Set) não são iteradores em si, mas todas herdam um método iterator, acessível usando o @@iterator símbolo bem conhecido, que retorna um objeto iterador criado a partir da estrutura de dados iterável:

const myIterable = [ 1, 2, 3 ];
const myIterator = myIterable[ Symbol.iterator ]();

myIterable;
> (3) [1, 2, 3]

myIterator;
> Array Iterator {}

Chamar o método next() em um iterador percorre os elementos que ele contém, um de cada vez. Cada chamada retorna um objeto com duas propriedades: value, que contém o valor do elemento atual, e done, um booleano que informa se o iterador transmitiu o último elemento da estrutura de dados. O valor de done é true somente quando uma chamada para next() resulta em uma tentativa de acessar um elemento além do último elemento no iterador.

const myIterable = [ 1, 2, 3 ];
const myIterator = myIterable[ Symbol.iterator ]();

myIterator.next();
> Object { value: 1, done: false }

myIterator.next();
> Object { value: 2, done: false }

myIterator.next();
> Object { value: 3, done: false }

myIterator.next();
> Object { value: undefined, done: true }

Funções do gerador

Use a palavra-chave function* (observe o asterisco) para declarar uma função de gerador ou definir uma expressão de função de gerador:

function* myGeneratorFunction() { };

Assim como os iteradores, as funções geradoras mantêm o estado. Chamar uma função de gerador retorna um novo objeto Generator, mas não executa imediatamente o código no corpo da função:

function* myGeneratorFunction() {
  console.log( "Generator function body ")
};
const myGeneratorObject = myGeneratorFunction();

myGeneratorObject;
> Generator {  }

typeof myGeneratorObject;
> "object"

Os objetos do gerador seguem o protocolo do iterador. O valor que cada chamada para next() em uma função de gerador retorna é determinado por uma expressão yield, que pausa a execução da função geradora e retorna o valor da expressão que contém a palavra-chave yield. Chamadas posteriores para next() continuam a execução da função, pausando na próxima expressão yield e retornando o valor associado.

function* myGeneratorFunction() {
  yield "My first yielded value.";
  yield "My second yielded value.";
};
const myGeneratorObject = myGeneratorFunction();

myGeneratorObject.next();
> Object { value: "My first yielded value.", done: false }

myGeneratorObject.next();
> Object { value: "My second yielded value.", done: false }

Quando next() é chamado depois que nenhum outro valor é especificado usando yield, return ou throw (em caso de erro), o restante do corpo da função é executado, e o objeto retornado tem um value de undefined e uma propriedade done de true:


function* myGeneratorFunction() {
    console.log( "Start of the generator function." );
    yield "First";
    console.log( "Second part of the generator function."  );
    yield "Second";
    console.log( "Third part of the generator function." );
    yield "Third";
};
const myGeneratorObject = myGeneratorFunction();

myGeneratorObject.next();
> "Start of the generator function."
> Object { value: "First", done: false }

myGeneratorObject.next();
> "Second part of the generator function."
> Object { value: "Second", done: false }

myGeneratorObject.next();
> "Third part of the generator function."
> Object { value: "Third", done: false }

myGeneratorObject.next();
> Object { value: undefined, done: true }

Use next() apenas no objeto que a função de gerador retorna, não na função de gerador em si. Caso contrário, cada chamada para a função gerador criará um novo objeto gerador:

function* myGeneratorFunction() {
  yield "First";
  yield "Second";
};

myGeneratorFunction().next();
> Object { value: "First", done: false }

myGeneratorFunction().next();
> Object { value: "First", done: false }

Como acontece com qualquer função, a função do gerador é interrompida quando encontra uma palavra-chave return. Em seguida, ele retorna um objeto ao contexto de invocação que contém o valor retornado e uma propriedade done com o valor true.

function* myGeneratorFunction() {
  yield 1;
  yield 2;
  return 3;
};
const myGeneratorObject = myGeneratorFunction();

myGeneratorObject.next().done;
> Object { value: 1, done: false }

myGeneratorObject.next().done;
> Object { value: 2, done: false }

myGeneratorObject.next();
> Object { value: 3, done: true }

Uma expressão yield pode assumir algumas das semânticas de um identificador, permitindo "comunicação" bidirecional de e para a parte suspensa da função do gerador. Quando um valor é transmitido para o método next() de um gerador como um argumento, ele substitui o valor associado à expressão yield suspensa anterior:

function* myGeneratorFunction() {
    const firstYield = yield;
    yield firstYield + 10;
};
const myGeneratorObject = myGeneratorFunction();

myGeneratorObject.next();
> Object { value: undefined, done: false }

myGeneratorObject.next( 5 );
> Object { value: 15, done: false }

Tenha em mente que isso substitui toda a expressão associada ao yield anterior e não apenas reatribui o valor do yield anterior ao valor especificado em next():

function* myGeneratorFunction() {
    const firstYield = yield;
    const secondYield = yield firstYield + 100;
    yield secondYield + 10;
};
const myGeneratorObject = myGeneratorFunction();

myGeneratorObject.next();
> Object { value: undefined, done: false }

myGeneratorObject.next( 10 ); // Can be thought of as changing the value of the `firstYield` variable to `10
> Object { value: 110, done: false }

myGeneratorObject.next( 20 ); // Can be thought of as changing the value of the `secondYield` variable to `20`, _not_ `20 + 100;`
> Object { value: 30, done: false }

Qualquer argumento transmitido para a primeira chamada para next() é ignorado, porque não há expressão yield anterior para aceitar esse valor. Como acontece com qualquer outra função, os argumentos passados para a chamada inicial da função do gerador estão disponíveis em todo o escopo do corpo da função do gerador:

function* myGeneratorFunction( startingValue ) {
    let newValue = yield startingValue + 1;
    newValue = yield newValue + 10;
    yield startingValue + 20;
};
const myGeneratorObject = myGeneratorFunction( 2 );

myGeneratorObject.next( 1 );
> Object { value: 3, done: false }

myGeneratorObject.next( 5 );
> Object { value: 15, done: false }

myGeneratorObject.next( 10 );
Object { value: 22, done: false }

O operador yield* (observe o asterisco) é usado com um iterável, como outra função geradora, para iterar e produzir cada valor que o operando retornar:

function* mySecondaryGenerator() {
  yield 2;
  yield 3;
}

function* myGenerator() {
  yield 1;
  yield* mySecondaryGenerator();
  yield 4;
  return 5;
}

const myIterator = myGenerator();

myIterator.next();
> Object { value: 1, done: false }

myIterator.next();
> Object { value: 2, done: false }

myIterator.next();
> Object { value: 3, done: false }

myIterator.next();
> Object { value: 4, done: false }

myIterator.next();
> Object { value: 5, done: true }

JavaScript assíncrono

Embora o JavaScript seja fundamentalmente síncrono na execução, existem mecanismos que permitem que os desenvolvedores aproveitem o loop de eventos para realizar tarefas assíncronas.

Promessas

Uma promessa é um marcador de posição para um valor que não é conhecido quando a promessa é criada. É um contêiner que determina uma operação assíncrona, os termos pelos quais a operação é considerada um sucesso ou falha, as ações a serem realizadas em qualquer caso e o valor resultante.

Crie uma instância de promessa usando o operador new com a função de construtor Promise integrada. Esse construtor aceita uma função chamada executor como argumento. Essa função de executor é normalmente usada para executar uma ou mais ações assíncronas e ditar os termos pelos quais a promessa vai ser considerada atendida ou rejeitada. Uma promessa é definida como pendente enquanto a função do executor está em execução. Depois que o executor termina, uma promessa é considerada atendida (ou resolvida, em algumas fontes de documentação) se a função do executor e a ação assíncrona que ele executa forem concluídas com sucesso e rejeitadas se a função do executor encontrar um erro ou a ação assíncrona que está sendo executada falhar. Depois que uma promessa é atendida ou rejeitada, ela é considerada liquidada.

const myPromise = new Promise( () => { });

O construtor chama a função do executor com dois argumentos. Esses argumentos são funções que permitem preencher ou rejeitar manualmente a promessa:

const  myPromise = new Promise( ( fulfill, reject ) => { });

As funções usadas para atender ou rejeitar uma promessa são chamadas com o valor resultante da promessa como um argumento (normalmente um erro de rejeição):

const myPromise = new Promise( ( fulfill, reject ) => {
  const myResult = true;
  setTimeout(() => {
    if( myResult === true ) {
        fulfill( "This Promise was successful." );    
    } else {
        reject( new Error( "This Promise has been rejected." ) );
    }
  }, 10000);
});

myPromise;
> Promise { <state>: "pending" }

myPromise;
> Promise { <state>: "fulfilled", <value>: "This Promise was successful." }

Encadeamento de promessas

O objeto Promise resultante pode ser realizado usando os métodos then(), catch() e finally() herdados do construtor de promessa. Cada um desses métodos retorna uma promessa, que pode ser executada imediatamente com then(), catch() ou finally() novamente, permitindo que você encadeie as promessas resultantes.

then() fornece duas funções de callback como argumentos. Use a primeira para cumprir a promessa resultante e a segunda para rejeitá-la. Os dois métodos aceitam um único argumento que atribui o valor à promessa resultante.

const myPromise = new Promise( ( fulfill, reject ) => {
  const myResult = true;
  setTimeout(() => {
    if( myResult === true ) {
        fulfill( "This Promise was fulfilled." );    
    } else {
        reject( new Error( "This Promise has been rejected." ) );
    }
  }, 100);
});

myPromise.then( successfulResult => console.log( successfulResult ), failedResult => console.error( failedResult ) );
> "This Promise was successful."

Você também pode usar then() para processar apenas o estado cumprido e catch para processar o estado rejeitado. Chame catch com um único argumento contendo o valor fornecido no método de rejeição da promessa:

const myPromise = new Promise( ( fulfill, reject ) => {
  const myResult = false;
  setTimeout(() => {
    if( myResult === true ) {
        fulfill( "This Promise was fulfilled." );    
    } else {
        reject( new Error( "This Promise has been rejected." ) );
    }
  }, 100);
});

myPromise
  .then( fulfilledResult => console.log(fulfilledResult ) )
  .catch( rejectedResult => console.log( rejectedResult ) )
  .finally( () => console.log( "The Promise has settled." ) );
> "Error: This Promise has been rejected."
> "The Promise has settled."

Ao contrário de then e catch, que permitem que uma função de gerenciador seja executada quando uma promessa é atendida ou rejeitada, uma função transmitida como um argumento para o método finally é chamada, independentemente de a promessa ter sido atendida ou rejeitada. A função do gerenciador é chamada sem argumentos, porque ela não se destina a trabalhar com os valores transmitidos da promessa, mas apenas para executar o código depois que a promessa for concluída.

Simultaneidade

O construtor de promessas fornece quatro métodos para trabalhar com várias promessas relacionadas, usando um iterável contendo objetos de promessa. Cada um desses métodos retorna uma promessa, que é atendida ou rejeitada com base no estado das promessas passadas a ela. Promise.all(), por exemplo, cria uma promessa que será atendida somente se todas as promessas passadas para esse método também forem atendidas:

const firstPromise  = new Promise( ( fulfill, reject ) => fulfill( "Successful. ") );
const secondPromise = new Promise( ( fulfill, reject ) => fulfill( "Successful. ") );
const thirdPromise  = new Promise( ( fulfill, reject ) => fulfill( "Successful. ") );
const failedPromise = new Promise( ( fulfill, reject ) => reject( "Failed.") );
const successfulPromises = [ firstPromise, secondPromise, thirdPromise ];
const oneFailedPromise = [ failedPromise, ...successfulPromises ];

Promise.all( successfulPromises )
  .then( ( allValues ) => {
    console.log( allValues );
  })
  .catch( ( failValue ) => {
    console.error( failValue );
  });
> Array(3) [ "Successful. ", "Successful. ", "Successful. " ]

Promise.all( oneFailedPromise  )
    .then( ( allValues ) => {
      console.log( allValues );
    })
    .catch( ( failValue ) => {
     console.error( failValue );
    });
> "Failed."

Os métodos de simultaneidade de promessas são os seguintes:

Promise.all()
Só cumprido se todas as promessas fornecidas forem atendidas.
Promise.any()
Atendido se qualquer uma das promessas fornecidas for atendida, e rejeitada apenas se todas as promessas forem rejeitadas.
Promise.allSettled()
Atendidos quando as promessas são resolvidas, independente do resultado.
Promise.race()
Rejeitada ou atendida com base no resultado da primeira promessa a ser resolvida, ignorando todas as promessas resolvidas posteriormente.

async/await

Quando você usa a palavra-chave async antes de uma declaração de função ou expressão de função, qualquer valor retornado pela função é retornado como uma promessa atendida que contém esse valor. Isso permite executar e gerenciar operações assíncronas usando os mesmos fluxos de trabalho do desenvolvimento síncrono.

async function myFunction() {
  return "This is my returned value.";
}

myFunction().then( myReturnedValue => console.log( myReturnedValue ) );
> "This is my returned value."

A expressão await pausa a execução de uma função assíncrona enquanto a promessa associada é resolvida. Depois que a promessa for resolvida, o valor da expressão await será o valor cumprido ou rejeitado da promessa.

async function myFunction() {
  const myPromise  = new Promise( ( fulfill, reject ) => { setTimeout( () => fulfill( "Successful. "), 5000 ); });
  const myPromisedResult = await myPromise;
  return myPromisedResult;
}

myFunction()
  .then( myResult => console.log( myResult ) )
  .catch( myFailedResult => console.error( myFailedResult ) );
> "Successful."

Qualquer valor não promissor incluído em uma expressão await é retornado como uma promessa cumprida:

async function myFunction() {
  const myPromisedResult = await "String value.";
  return myPromisedResult;
}

myFunction()
  .then( myResult => console.log( myResult ) )
  .catch( myFailedResult => console.error( myFailedResult ) );
> "String value."

Teste seu conhecimento

Que tipo de repetição você usa para iterar em uma quantidade conhecida?

for
while
do...while