Pontos-Chave do JavaScript: Entendendo Event Loop, Memória, Protótipos e Recursos Modernos

A compreensão dos conceitos fundamentais do JavaScript é essencial para escrever código eficiente e confiável. Abaixo, exploramos aspectos cruciais como o mecanismo de execução assíncrona, gerenciamento de memória, sistema de herança e recursos contemporâneos.

  1. Ciclo de Eventos (Event Loop)

O ciclo de eventos é o coração do tratamento assíncrono no JavaScript, permitindo operações não bloqueantes em uma linguagem de thread única. Ele gerencia filas de tarefas de diferentes prioridades.

Macrotarefas vs. Microtarefas

  • Macrotarefas: Tarefas originadas pelo ambiente (navegador ou Node.js). Exemplos incluem:
    • setTimeout e setInterval
    • setImmediate (exclusivo do Node.js)
    • Operações de I/O (leitura de arquivos, requisições de rede)
    • Renderização da interface (navegador)
    • O código principal do script
  • Microtarefas: Tarefas iniciadas pelo próprio JavaScript, frequentemente ligadas a promessas. Exemplos:
    • Promise.then(), Promise.catch(), Promise.finally()
    • process.nextTick (Node.js, com prioridade máxima)
    • MutationObserver (navegador)

Regras de Execução no Navegador

  1. Execute uma macrotarefa (geralmente a mais antiga na fila), como o script global.
  2. Durante a execução, se encontrar microtarefas, coloque-as na fila de microtareafs; se encontrar macrotarefas, coloque-as na fila de macrotarefas.
  3. Após a conclusão da macrotarefa atual, processe imediatamente todas as microtarefas na fila.
  4. Realize a renderização da interface, se necessário.
  5. Retire a próxima macrotarefa da fila e comece um novo ciclo.

Princípio fundamental: uma macrotarefa → todas as microtarefas → (renderização) → próxima macrotarefa.

Diferenças entre Node.js e Navegador

O Node.js baseia seu ciclo de eventos na biblioteca libuv, com fases mais complexas.

Fases simplificadas no Node.js:

  1. Timers: Executa callbacks de setTimeout e setInterval.
  2. Pending callbacks: Processa callbacks de I/O atrasados.
  3. Idle, Prepare: Uso interno.
  4. Poll: Obtém novos eventos de I/O e executa seus callbacks (pode bloquear se necessário).
  5. Check: Executa callbacks de setImmediate.
  6. Close callbacks: Processa eventos de encerramento (ex.: socket.on('close', ...)).

process.nextTick() é executado entre as fases, com prioridade superior às microtarefas comuns.

Exemplo de Execução Assíncrona Complexa


console.log('Início da execução');
setTimeout(() => {
  console.log('Execução do setTimeout');
  Promise.resolve().then(() => {
    console.log('Promessa dentro do setTimeout');
  });
}, 0);
Promise.resolve()
  .then(() => {
    console.log('Primeira promessa');
  })
  .then(() => {
    console.log('Segunda promessa');
  });
console.log('Fim da execução');
// Saída esperada:
// Início da execução
// Fim da execução
// Primeira promessa
// Segunda promessa
// Execução do setTimeout
// Promessa dentro do setTimeout

  1. Gerenciamento de Memória e Coleta de Lixo

O JavaScript aloca e libera memória automaticamente por meio da coleta de lixo (Garbage Collection).

Mecanismo de Coleta de Lixo no V8

O V8 emprega uma coleta de lixo baseada em gerações, dividindo a memória em:

  1. Espaço Novo (New Space):
    • Armazena objetos de vida curta (ex.: variáveis locais).
    • Utiliza o algoritmo Scavenge (uma cópia eficiente), que move objetos sobreviventes de From-Space para To-Space.
    • Objetos que sobrevivem múltiplas coletas são promovidos ao espaço antigo.
  2. Espaço Antigo (Old Space):
    • Armazena objetos de vida longa ou grandes (ex.: variáveis globais, closures).
    • Usa algoritmos como Marcação-Limpeza (Mark-Sweep) e Marcação-Compactação (Mark-Compact).
    • Marcação-Limpeza identifica objetos alcançáveis a partir de raízes e remove os não marcados; Marcação-Compactação reorganiza a memória para reduzir fragmentação.

Referência contagem é uma técnica antiga e ineficaz para ciclos de referência.

Cenários Comuns de Vazamento de Memória

  • Variáveis globais acidentais: Em modo não estrito, atribuir a variáveis não declaradas cria globais.
  • Timers ou callbacks esquecidos: Callbacks que mantêm referências a grandes objetos podem impedir a coleta.
  • Referências a nós do DOM removidos: Manter referências a elementos DOM já removidos do documento.
  • Closures: Closures podem reter referências a variáveis de escopo externo desnecessárias.

Depuração com Chrome DevTools

  • Painel Performance: Grava o desempenho para verificar se a memória do heap cresce continuamente.
  • Painel Memory: Use Heap Snapshots para comparar alocações ao longo do tempo, ou Allocation instrumentation para rastrear distribuições de memória em tempo real.
  1. Protótipos e Herança

JavaScript utiliza cadeias de protótipos para implementar herança.

Cadeia de Protótipos

  • Cada função tem uma propriedade prototype (objeto protótipo).
  • Cada objeto tem uma propriedade __proto__ (referência ao protótipo de seu construtor).
  • Ao acessar uma propriedade, se não existir no objeto, a busca segue a cadeia __proto__ até encontrar ou atingir null.

function Pai() {
  this.nome = 'Pai';
}
Pai.prototype.dizerNome = function() {
  console.log(this.nome);
};

function Filho() {}
Filho.prototype = new Pai(); // ou Filho.prototype = Object.create(Pai.prototype)
Filho.prototype.constructor = Filho;

const filho = new Filho();
filho.dizerNome(); // 'Pai'

Sintaxe de Classe

A palavra-chave class no ES6 é uma simplificação sintática para herança baseada em protótipos.


class Pai {
  constructor() {
    this.nome = 'Pai';
  }
  dizerNome() {
    console.log(this.nome);
  }
}

class Filho extends Pai {
  constructor() {
    super();
  }
}
// A estrutura do protótipo é semelhante ao exemplo anterior.

Padrões de Herança e Trade-offs

  1. Herança via protótipo: Simples, mas propriedades compartilhadas podem causar efeitos colaterais; sem passagem de parâmetros.

  2. Herança via construtor: Resolve compartilhamento e permite parâmetros, mas métodos não são reutilizáveis.

  3. Herança combinada: Combina as duas abordagens acima, amplamente usada, mas invoca o construtor pai duas vezes.

  4. Herança prototípica (com Object.create()): Cria objetos sem construtor, mas propriedades são compartilhadas.

  5. Herança parasitária combinada: Abordagem otimizada que invoca o construtor pai uma vez e evita propriedades desnecessárias no protótipo do filho. É a base da implementação de extends.

  6. Recursos Modernos do ES6+


Promise

Promessas gerenciam operações assíncronas, evitando "callback hell". Estados: pending, fulfilled, rejected. APIs como .then(), .catch(), Promise.all().

Async/Await

Sintaxe que simplifica código assíncrono, baseada em promessas. Funções async retornam promessas; await pausa execução até que a promessa resolva.

Geradores (Generators)

Funções que podem pausar e retomar com function* e yield. Fundamentam a implementação do Async/Await.


function* sequencia() {
  yield 'primeiro';
  yield 'segundo';
}
const gen = sequencia();
gen.next(); // { value: 'primeiro', done: false }

Módulos (ES Module)

Solução de modularização nativa com export e import. Ético e estático, permitindo otimizações como Tree Shaking.

Funções Arrow

Sintaxe concisa; não possuem this próprio (herdam do escopo léxico); não têm arguments e não podem ser usadas como construtoras.

Desestruturação

Extrai valores de arrays ou objetos para variáveis.


// Array
const [x, y] = [10, 20];
// Objeto
const { id, valor } = { id: 1, valor: 'exemplo' };
// Em parâmetros de função
function processar({ chave, dados }) { /* ... */ }

Tags: event-loop garbage-collection prototypal-inheritance ES6 async-await

Publicado em 8-4 23:31