Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

Function expression vs declaration em JavaScript

As duas formas de criar função em JavaScript não são intercambiáveis: hoisting, nome na stack trace e IIFE, com o erro de cada escolha rodando no Node 24.

Rodolfo Mori7 min de leitura

Existem duas formas de criar uma função com a palavra function, e a diferença entre elas não é estética. Function declaration começa a linha com function e é içada inteira para o topo do escopo — dá para chamar antes de escrever. Function expression guarda a função numa variável e só existe a partir da linha da atribuição.

Na prática: declaration você chama de qualquer lugar do escopo; expression só depois. Chamar antes gera um erro diferente conforme a variável seja var, let ou const — e os três aparecem reproduzidos aqui, com o rastro real do Node 24.16.0.

Todos os exemplos usam a mesma loja virtual das lições anteriores da trilha de JavaScript: subtotal, desconto, carrinho.

O comportamento recebe dois nomes precisos: function declaration e function expression. Em palavras simples, uma já é registrada com a função pronta; a outra depende de a execução chegar à atribuição da variável.

A loja que abre cedo e o balcão montado depois

Imagine uma loja cujo endereço já consta no mapa quando o dia começa, enquanto um balcão promocional só passa a existir depois que a equipe o monta. A declaration se parece com a loja: o escopo registra nome e função antes de rodar a primeira linha. A expression se parece com o balcão: a variável segue as regras de var, let ou const e só recebe a função na atribuição.

Antes de executar os dois primeiros casos, faça uma tabela com “nome existe?”, “valor atual” e “pode chamar?”. Preencha cada coluna para a linha anterior à declaração e compare com a saída. Isso mostra por que os erros mudam sem tratar hoisting como mágica.

js
function calcularSubtotal(preco, quantidade) {
  return preco * quantidade;
}

const calcularDesconto = function (valor, percentual) {
  return valor * (percentual / 100);
};

console.log(calcularSubtotal(89.9, 3).toFixed(2));
console.log(calcularDesconto(269.7, 15).toFixed(2));
console.log(typeof calcularSubtotal, '|', typeof calcularDesconto);
console.log(calcularSubtotal.name, '|', calcularDesconto.name);
269.70 40.45 function | function calcularSubtotal | calcularDesconto

Depois de criadas, as duas são indistinguíveis: mesmo typeof, mesmo jeito de chamar, e até o mesmo .name — o motor deduz o nome da expression a partir da variável que a recebeu. A separação existe antes dessas linhas.

Repare no ponto e vírgula depois da chave de fechamento da expression. Ele não é decorativo: aquilo é uma instrução de atribuição, como const total = 10;. A declaration não leva ponto e vírgula, porque não é atribuição nenhuma.

Hoisting: a diferença que aparece antes da linha

Uma declaration é registrada no escopo antes de qualquer linha rodar. Chamar em cima da definição funciona:

js
console.log(calcularSubtotal(89.9, 3).toFixed(2));

function calcularSubtotal(preco, quantidade) {
  return preco * quantidade;
}
269.70

Com expression, o que é içado é a variável, não a função. Guardada em var, a variável existe desde o topo valendo undefined:

js
console.log(calcularDesconto(269.7, 15));

var calcularDesconto = function (valor, percentual) {
  return valor * (percentual / 100);
};
file:///private/tmp/loja/expr-var.mjs:1 console.log(calcularDesconto(269.7, 15)); ^

TypeError: calcularDesconto is not a function at file:///private/tmp/loja/expr-var.mjs:1:13 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)

Node.js v24.16.0

Leia a mensagem ao pé da letra: calcularDesconto existe, e o que ela vale não é chamável. Vale undefined, e o motor está reclamando de você ter posto parênteses depois de um undefined.

Trocando var por const, o diagnóstico melhora:

js
console.log(calcularDesconto(269.7, 15));

const calcularDesconto = function (valor, percentual) {
  return valor * (percentual / 100);
};
file:///private/tmp/loja/expr-const.mjs:1 console.log(calcularDesconto(269.7, 15)); ^

ReferenceError: Cannot access ‘calcularDesconto’ before initialization at file:///private/tmp/loja/expr-const.mjs:1:13 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)

Node.js v24.16.0

Agora o erro diz exatamente o que aconteceu: você chegou cedo demais. É a zona morta temporal de let e const, o mesmo mecanismo detalhado em variáveis em JavaScript.

Nomeada ou anônima: o que muda na stack trace

Uma expression pode ter nome próprio, escrito entre function e os parênteses. Esse nome não vaza para fora — ele só serve dentro da própria função e nas mensagens de erro. E é aí que ele paga o investimento:

js
const validarAnonima = function (pedido) {
  return pedido.itens.length;
};

const validarNomeada = function contarItensDoPedido(pedido) {
  return pedido.itens.length;
};

console.log('name:', JSON.stringify(validarAnonima.name), '|', JSON.stringify(validarNomeada.name));

for (const fn of [validarAnonima, validarNomeada]) {
  try {
    fn({});
  } catch (e) {
    console.log(e.stack.split('\n')[1].trim());
  }
}
name: "validarAnonima" | "contarItensDoPedido" at validarAnonima (file:///private/tmp/loja/stack-nome.mjs:2:23) at contarItensDoPedido (file:///private/tmp/loja/stack-nome.mjs:6:23)

No primeiro caso o motor emprestou o nome da variável, e isso funciona porque a função foi atribuída a um const. Quando você passa a função direto como argumento — itens.map(function (item) { ... }) — não existe variável para emprestar nome nenhum, e o rastro sai com <anonymous>. Em um map aninhado em outro map, três <anonymous> seguidos custam caro.

O nome também permite a função chamar a si mesma sem depender da variável de fora:

js
const somarItens = function somar(itens, i = 0) {
  if (i >= itens.length) return 0;
  return itens[i].preco + somar(itens, i + 1);
};

const carrinho = [
  { nome: 'Teclado mecânico', preco: 289.9 },
  { nome: 'Mouse sem fio', preco: 149.9 },
  { nome: 'Mousepad', preco: 39.9 },
];

console.log(somarItens(carrinho).toFixed(2));
console.log('o nome interno vaza para fora?', typeof somar);
479.70 o nome interno vaza para fora? undefined

somar funciona dentro da função e não existe fora dela. Se a recursão dependesse de somarItens, bastaria alguém reatribuir essa variável em outro ponto do arquivo para a recursão apontar para o lugar errado.

IIFE: o padrão que existia antes dos módulos

Uma expression pode ser chamada na hora em que é criada. Envolva a função em parênteses e ponha () no fim: é a Immediately Invoked Function Expression.

js
var precoFinal = (function () {
  var taxa = 0.18;
  var base = 289.9;
  return base * (1 + taxa);
})();

console.log(precoFinal.toFixed(2));
console.log('taxa vazou?', typeof taxa);
342.08 taxa vazou? undefined

Os parênteses de fora não são enfeite: sem eles, a linha começaria com a palavra function e o motor leria uma declaration — que não pode ser chamada na mesma linha. Os parênteses forçam o contexto de expressão.

Antes de 2015 não havia let, const nem módulos ES: tudo que você declarasse num arquivo .js de página caía no mesmo escopo global, e dois scripts com uma variável total brigavam. A IIFE era a única forma barata de fechar uma caixa. Da mesma ideia nasceu o module pattern, que ainda aparece em bibliotecas antigas:

js
const carrinho = (function () {
  const itens = [];

  return {
    adicionar(item) {
      itens.push(item);
      return itens.length;
    },
    total() {
      return itens.reduce((soma, item) => soma + item.preco, 0);
    },
  };
})();

console.log(carrinho.adicionar({ nome: 'Teclado mecânico', preco: 289.9 }));
console.log(carrinho.adicionar({ nome: 'Mouse sem fio', preco: 149.9 }));
console.log(carrinho.total().toFixed(2));
console.log('itens por fora:', carrinho.itens);
1 2 439.80 itens por fora: undefined

itens não é acessível de fora e mesmo assim os dois métodos continuam enxergando o array. Isso é closure, e é o assunto de uma lição inteira mais adiante nesta trilha.

Expression brilha quando a função é uma escolha

Como expression é um valor, ela participa de if, de objeto e de array. Uma declaration não faz isso — ela é fixa no escopo.

js
const ambiente = process.env.NODE_ENV ?? 'desenvolvimento';

let registrarEvento;

if (ambiente === 'producao') {
  registrarEvento = function enviarParaAnalytics(nome) {
    return `analytics: ${nome}`;
  };
} else {
  registrarEvento = function imprimirNoConsole(nome) {
    return `console: ${nome}`;
  };
}

console.log(registrarEvento('checkout_iniciado'));
console.log('quem atendeu:', registrarEvento.name);
console: checkout_iniciado quem atendeu: imprimirNoConsole

Aqui os nomes internos se pagam sozinhos: o log diz qual das duas implementações respondeu. Com funções anônimas, registrarEvento.name sairia vazio nos dois casos e o log não valeria nada.

Erros comuns: chamar a expression cedo demais

O erro característico desta lição já apareceu acima em forma pura, mas na vida real ele vem disfarçado — dentro de um export, de um setTimeout ou de um arquivo de configuração lido de cima para baixo:

js
inicializarLoja();

const conectarBanco = function () {
  return 'conectado';
};

function inicializarLoja() {
  return conectarBanco();
}
file:///private/tmp/loja/init-loja.mjs:8 return conectarBanco(); ^

ReferenceError: Cannot access ‘conectarBanco’ before initialization at inicializarLoja (file:///private/tmp/loja/init-loja.mjs:8:3) at file:///private/tmp/loja/init-loja.mjs:1:1 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)

Node.js v24.16.0

inicializarLoja é uma declaration e por isso pôde ser chamada na linha 1 sem reclamação. O problema é que, quando ela rodou, conectarBanco ainda não tinha sido inicializada. O rastro entrega isso de graça: a linha 1 chamou, a linha 8 quebrou.

Duas correções, e a segunda é melhor. A rápida: mover inicializarLoja() para o fim do arquivo. A definitiva: transformar conectarBanco em declaration também — aí a ordem das linhas para de importar.

Qual usar em cada situação

situação forma por quê
função principal do módulo declaration pode ficar no fim do arquivo e ser chamada em cima
callback passado para map, filter, setTimeout expression (em geral arrow) é um valor de uso único; nomear ajuda no rastro
implementação escolhida em tempo de execução expression função como valor entra em if, objeto e array
método dentro de objeto ou classe sintaxe curta de método precisa do this da chamada
isolar escopo em script sem bundler IIFE código legado; em módulo ES é desnecessário

A regra que eu passo em aula: se você conseguiria explicar a função pelo nome dela numa conversa — “essa é a que calcula o frete” — ela merece ser uma declaration, com nome fixo e posição livre no arquivo. Se ela é um detalhe de uma linha dentro de outra operação, é expression.

A próxima lição pega a forma mais comum de expression no código moderno e mostra o que ela ganhou e o que perdeu: arrow function em JavaScript. Para ver onde esta lição se encaixa no todo, o guia completo de JavaScript tem o mapa da trilha.

  • function expression
  • function declaration
  • hoisting
  • iife
  • stack trace

Perguntas frequentes

Qual das duas formas eu devo usar no dia a dia?
Declaration para as funções que são o assunto do arquivo, porque você pode colocá-las no fim e chamar em cima. Expression quando a função é um valor que você guarda, passa adiante ou escolhe em tempo de execução. A maior parte dos guias de estilo aceita as duas, desde que o arquivo seja coerente consigo mesmo.
Function expression é a mesma coisa que arrow function?
Não. Arrow function é uma sintaxe de expression, mas nem toda expression é arrow. A diferença entre elas não é o hoisting — é this, arguments, prototype e a possibilidade de usar new, que a arrow não tem.
Vale a pena nomear uma function expression?
Vale, e custa uma palavra. O nome aparece na stack trace do erro, permite a função chamar a si mesma sem depender da variável externa, e não vaza para o escopo de fora. Sem nome, o motor herda o nome da variável — o que funciona até você passar a função direto como argumento.
IIFE ainda faz sentido com módulos ES?
Para isolar escopo, não: o módulo já faz isso, e nada declarado nele escapa para o global. IIFE ainda aparece em snippet colado no console, em script antigo sem bundler e em código gerado por ferramenta. Saber ler é obrigatório; escrever, quase nunca.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — function expression — developer.mozilla.org
  2. MDN — IIFE — developer.mozilla.org
  3. ECMAScript 2026 — Function Definitions — tc39.es

Continue por aqui