← JavaScript: linguagem e diagnóstico de aplicações
09 / 10 · 50 MIN

Módulos e limites do ambiente de execução

Interpreta imports e exports como contratos entre módulos; distingue bindings vivos, snapshots e regras de carregamento do ambiente.

Nomes locais e contrato exportado

Um export nomeado é identificado pelo nome publicado. import { parse as read } declara read localmente, mas continua a pedir o export parse. Um export por omissão ocupa a entrada default e pode ser recebido com outro nome local, como import render from "./format.js". Declarar export default function format não cria, por si só, um segundo export nomeado format.

import * as config fornece um namespace que permite consultar exports pelas suas propriedades. Esse objeto não é o mesmo que a função por omissão nem é um saco de valores editáveis. Ao diagnosticar um import, separa três perguntas: o módulo foi encontrado, publicou o nome pedido e esse valor tem o contrato que o consumidor espera? Alterar o nome local não corrige um caminho inexistente nem cria um export em falta.

Bindings vivos e snapshots explícitos

Um import nomeado é uma ligação indireta ao binding do exportador. Se o exportador reatribui um let exportado, uma leitura posterior pelo import observa o novo valor. O consumidor não pode reatribuir diretamente esse binding importado. Porém, se o valor exportado é um objeto comum, a restrição do binding não congela as suas propriedades: outro consumidor pode observar alterações nesse objeto partilhado.

const initial = rate copia o valor lido naquele instante para uma variável local. Se rate é um Number, initial conserva esse número depois de o exportador mudar rate. Se o valor fosse um objeto, copiar apenas a referência continuaria a partilhar o objeto. Decide se o consumidor precisa de estado corrente ou de um snapshot independente e constrói esse contrato deliberadamente.

Inicialização, modo estrito e ciclos

Módulos usam modo estrito e o seu this de nível superior é undefined. Os nomes definidos no módulo não passam automaticamente a propriedades de window. Imports estáticos pertencem ao nível superior do módulo; não são declarações para colocar dentro de um if ou de uma função. O grafo de dependências participa na ligação e avaliação antes de o consumidor executar o seu corpo normal.

Num ciclo, um módulo pode tentar ler um binding que o outro ainda não inicializou. Essa leitura pode lançar ReferenceError, apesar de o nome estar corretamente exportado. Nem todos os ciclos falham: o momento de uso é decisivo. Uma correção sustentável pode extrair a dependência comum ou tornar a inicialização explícita. Acrescentar um atraso arbitrário para esconder a ordem não clarifica o contrato e pode criar outra falha temporal.

Resolver e entregar o módulo no navegador

Num navegador sem import map ou loader especial, "./lib/parse.js" é resolvido relativamente ao URL do módulo importador. Para /app/main.js, isso aponta para /app/lib/parse.js. Um nome simples de pacote, como "parser-lib", precisa de um mapeamento ou de outra ferramenta que o resolva; o navegador não adota automaticamente a pesquisa de pacotes de outro ambiente. A resolução do especificador é uma responsabilidade do host.

O elemento script com type="module" declara a entrada como módulo. Defer sozinho não transforma um script clássico num módulo. Confirma também o recurso realmente entregue: HTTP 200 com Content-Type text/html e corpo de página inicial pode ser um fallback do servidor para um caminho inexistente. Um módulo JavaScript precisa de conteúdo e tipo MIME adequados. O estado HTTP, o tipo do recurso e os exports são verificações diferentes.

Carregamento dinâmico e dependências do ambiente

import("./report.js") é uma expressão que devolve uma promise; no sucesso, o consumidor recebe o namespace do módulo. Pode servir um caminho opcional da aplicação, mas o chamador continua responsável por acompanhar rejeições e verificar o export usado. Não é uma forma de obter imediatamente a função default nem de ler apenas uma string com o código-fonte.

O formato de módulo não fornece DOM, sistema de ficheiros ou outras APIs ausentes. Se uma regra de cálculo lê document durante a avaliação inicial, um consumidor sem DOM falha antes de conseguir usar a regra. Receber os dados como argumentos e manter a leitura do ambiente num adaptador permite reutilizar a lógica. Um import dinâmico pode adiar a avaliação, mas não elimina essa dependência quando o módulo é finalmente usado.

Exemplo orientado: política corrente e valor guardado

Os três ficheiros do exemplo partilham o mesmo módulo state.js. main.js lê limit quando vale 10 e guarda esse Number em originalLimit. A chamada accepts(12) devolve false. setLimit(20) valida e reatribui o binding exportado. A chamada posterior accepts(12) devolve true porque policy.js lê o limit corrente através do seu import. No fim, originalLimit é 10 e limit é 20.

Se setLimit recebe a string "20", Number.isInteger rejeita-a e a atribuição não ocorre. Não há conversão implícita. A política aceita units inteiras não negativas até ao limite corrente; não valida outras dimensões de negócio. Esta escolha de estado partilhado é intencional no exemplo. Para decisões que precisam de reprodutibilidade histórica, passar um limite explícito capturado na aprovação cria um contrato diferente e mais apropriado do que ler o valor corrente mais tarde.

Uma sequência de diagnóstico verificável

Começa pelo contexto de execução: módulo nativo ou script clássico, ambiente com ou sem DOM e ferramenta que resolve os nomes. Depois segue o URL ou especificador até ao recurso recebido. Só após confirmar entrega e tipo faz sentido diagnosticar o nome exportado, a inicialização e o uso do valor. Esta sequência evita reescrever uma função correta quando o servidor entregou HTML no lugar do ficheiro.

Para estado partilhado, regista o momento da leitura e quem pode escrever. Um import vivo, uma cópia de primitivo e uma referência partilhada não são equivalentes. Ao responder aos casos, escolhe a alteração que resolve a fronteira demonstrada pelos factos. Não é necessário executar os ficheiros: os excertos e as premissas identificam os contratos e a ordem relevantes.

// state.js
export let limit = 10;
export function setLimit(value) {
  if (!Number.isInteger(value) || value < 0 || value > 50) {
    throw new TypeError("limit");
  }
  limit = value;
}

// policy.js
import { limit } from "./state.js";
export function accepts(units) {
  return Number.isInteger(units) && units >= 0 && units <= limit;
}

// main.js
import { limit, setLimit } from "./state.js";
import { accepts } from "./policy.js";
const originalLimit = limit;
const before = accepts(12);
setLimit(20);
const after = accepts(12);
NA PRÁTICA

Os comentários delimitam três ficheiros distintos, não um único ficheiro a concatenar. Assume módulos ES nativos, os mesmos URLs resolvidos para state.js, métodos padrão e nenhuma outra alteração. Prevê originalLimit, limit, before e after após a avaliação de main.js.

Armadilhas comuns

Tratar import como uma cópia; confundir default com export nomeado; supor que uma extensão fornece APIs; ler bindings num ciclo antes da inicialização; aceitar HTTP 200 como prova de entrega do módulo correto.

Tópicos relacionados: Variáveis, funções e closures · Objetos, identidade e limites das propriedades · Promises, async e propagação de erros · Estado, ordem e diagnóstico de integração

Leva esta ideia contigo

O contrato de módulo inclui nomes exportados, identidade e momento de leitura, além da resolução e das APIs que o ambiente fornece.

Criar conta

Referência: CreateImportBinding · ECMAScript 2026 (17th edition); browser APIs identified separately