Como usar uma IIFE (função autoinvocada) para criar um código modular e sem conflitos de escopo

Desvende o super poder das IIFEs, ou funções autoinvocadas, para criar um código modular e sem conflitos de escopo.

  • iife
  • immediately invoked function expression
  • funções imediatas
  • javascript
  • encapsulamento
Front-end

IIFE não tem nada a ver com o if de if else (queria tirar esse elefante branco da sala 🤣).

A IIFE, ou Immediately Invoked Function Expression, é a declaração e execução de uma função juntas.

Declaração? Execução? Relembre!

Uma função em Javascript tem 2 "momentos":

  1. Declaração

  2. Execução/invocação

// Declaração

function displayName(name) {
  console.log(name);
}

// Execução

displayName('Menezes');

Legal, mas... para quê?

O que é e para que serve uma IIFE? A função invocada imediatamente

Uma IIFE é a declaração e execução de uma função Javascript em apenas uma expressão.

Estruturalmente, escrever uma IIFE é bastante simples. Primeiro, escreva uma função normal:

function() {
  console.log('Menezes');
}

Agora coloque toda essa declaração dentro de um parênteses:

(function() {
  console.log('Menezes');
})

E para fechar, adicione outro parênteses no final:

(function() {
  console.log('Menezes');
})();

O primeiro parênteses recebe a declaração da função. E o segundo, os argumentos da execução, ou fica vazio.

Vou modificar ela um pouco, e informar o nome que quero mostrar no parâmetro:

(function(name) {
  console.log(name);
})('Menezes');

Veja o que aconteceu na ordem de execução:

  • Linha 3: informo a string 'Menezes' como argumento para a IIFE dentro do parênteses de execução

  • Linha 1: a declaração da IIFE recebe esse argumento como name no parâmetro

  • Linha 2: o corpo da função usa o parâmetro name para escrever seu valor no console

E isso tudo aconteceu "de uma vez".

Quase sempre, a função que as pessoas declaram em uma IIFE é anônima, ou seja, não tem nome. Por isso você também pode chamá-la de Self-Executing Anonymous Function .

É claro que você pode nomeá-la, mas vai servir apenas como organização. Isso porque, logo após executar a IIFE, você não pode executá-la pelo nome:

(function showName(name) {
  console.log(name);
})('Menezes'); // Resultado: 'Menezes'

showName('André'); // Resultado: ReferenceError: showName is not defined

Caso você queira chamar a função novamente no futuro, não crie uma IIFE:

function showName(name) {
  console.log(name);
}

showName('Menezes'); // Resultado: 'Menezes'

showName('André'); // Resultado: 'André'

Uma última característica importante, é sobre atribuir uma IIFE a uma variável. Ela irá receber o retorno da função, e não a declaração dela.

Veja esse exemplo:

const result = function(number) {
  return number + 5;
}

Dessa forma, result é uma função que recebe o parâmetro number e retorna ele mais 5.

Porém assim o cenário é outro:

const result = (function(number) {
  return number + 5;
})(3);

Agora, result é o próprio retorno da função, ou seja, o resultado da soma. Nesse caso, 8.

Quando vi uma IIFE pela primeira vez, fiquei com uma dúvida. Por que não executar as linhas diretamente sem envolver numa função anônima?

Vantagens de criar uma IIFE

Em resumo, são duas:

  1. Evitar conflito de variáveis com o encapsulamento

  2. Imutabilidade

Evitar conflito de variáveis com o encapsulamento

O velho problema de nomear as coisas.

Veja esse exemplo:

function soma(numA, numB) {
  return numA + numB;
}

var result = 0;
console.log(result); // 0

// algumas linhas depois...

var result = soma(4, 8);
console.log(result); // 12

// muitas linhas depois...

var result = soma(12, 3);
console.log(result); // 15

// a lot of linhas depois...

console.log(result); // ???

Aqui eu mostrei que a variável result recebe diferentes valores ao longo do arquivo. E isso dificulta saber qual é o seu valor na última linha. Claro, em um arquivo pequeno fica fácil, mas em outro cenário isso seria bem trabalhoso.

E se sua página importa mais de um arquivo Javascript, esse conflito de nome de variáveis também acontece entre eles.

Se criasse com const, isso já geraria um erro.


Por outro lado, com IIFEs isso não acontece:

function soma(numA, numB) {
  return numA + numB;
}

var result = 0;
console.log(result); // 0

// algumas linhas depois...

(function() {
  var result = soma(4, 8);
  console.log(result); // 12
})();

// muitas linhas depois...

(function() {
  var result = soma(12, 3);
  console.log(result); // 15
})();

// a lot of linhas depois...

console.log(result); // 0

Com as IIFEs, o valor que declarei para a variável result não altera a primeira ocorrência dessa variável. E assim fica fácil saber que, na última linha, ela mantém o mesmo valor de sua declaração inicial.

Mas e se eu precisasse usar o valor de result dentro de uma a IIFE?

Imutabilidade

A imutabilidade é um assunto muito importante dentro do paradigma de programação funcional.

Veja o exemplo abaixo:

var code = 10;

(function(code) {
  code = code + 5;
  console.log(code); // 15
})(code);

console.log(code) // 10

Mesmo que você altere o valor da variável code dentro da IIFE, no final do código ela mantém o valor inicial, 10.

No seu dia a dia, você pode encontrar outras vantagens de usar uma IIFE. E já que falei da sua rotina de trabalho, veja agora casos de uso de uma função autoinvocada.

Exemplos de uso de IIFE

Proteger variáveis do escopo global

Isso fica na conta do encapsulamento.

Se você viu a maioria dos exemplos que mostrei até aqui, então já entendeu.

Uma IIFE cria um escopo de função que evita conflitos. Assim, as variáveis que você criou em uma IIFE, não conflita com as de outros escopos. Isso envolve conflitar tanto com as suas variáveis, quanto com as de bibliotecas externas. Apesar de que esses pacotes já envolvem suas funções e variáveis em blocos de funções.

E já que elas não estão em conflito, fica mais confortável criar variáveis e função para cada parte do código.

Posteriormente, você pode inicializar esses blocos um por vez.

Inicializar blocos de código

Imagine uma página com dois modais um pouco diferentes.

Um deles (modalA) possui um backdrop que ao clicar fecha o modal. Já o outro, modalB, não, é um modal bloqueante.

Apesar dessa diferença, ambos são modais, e compartilham alguns comportamentos:

// Funções compartilhadas

function openModal() { /* ... */ }
function closeModal() { /* ... */ }

// modalA: não bloqueante

const modal = document.querySelector('.modalA .modal');
const backdrop = document.querySelector('.modalA .backdrop');
  
backdrop.addEventListener('click', closeModal);

// modalB: bloqueante

const modal = document.querySelector('.modalB .modal');
const backdrop = document.querySelector('.modalB .backdrop');
  
// não adiciona evento de clique no backdrop

Se você iniciar os modais assim, haverá um conflito das variáveis modal e backdrop.

Para resolver isso, envolva cada modal em uma IIFE que inicializa seus comportamentos:

// Funções compartilhadas

function openModal() { /* ... */ }
function closeModal() { /* ... */ }

// modalA: não bloqueante

(function modalA() {
  const modal = document.querySelector('.modalA .modal');
  const backdrop = document.querySelector('.modalA .backdrop');
  
  backdrop.addEventListener('click', closeModal);
})();

// modalB: bloqueante

(function modalB() {
  const modal = document.querySelector('.modalB .modal');
  const backdrop = document.querySelector('.modalB .backdrop');
  
  // não adiciona evento de clique no backdrop
})();

No exemplo acima, criei funções não anônimas nas IIFEs apenas para organização.

Além de inicializar componentes, você pode preparar módulos e usá-los no futuro.

Modularidade

Esse foi o exemplo mais interessante de criar.

Com uma immediately invoked function expression você pode criar módulos. Esses módulos criam valores privados que não podem ser alterados de fora. Apenas os métodos que você escolhe expor, por meio do return, podem alterar os valores privados.

Veja o código abaixo, e em seguida eu explico o que está acontece:

const contaCorrente = (function() {
  let saldo = 0;

  function depositar(value) {
    saldo = saldo + value;
  }

  function sacar(value) {
    saldo = saldo - value;
  }

  function verSaldo() {
    return saldo;
  }

  return { depositar, sacar, verSaldo };
})();

contaCorrente.depositar(30);
contaCorrente.depositar(40);
contaCorrente.sacar(50);
contaCorrente.sacar(60);

console.log(contaCorrente.verSaldo()); // -40

contaCorrente.depositar(100);

console.log(contaCorrente.verSaldo()); // +60

Eu criei uma contaCorrente que recebe o retorno de uma IIFE. Esse retorno, como você pode ver na linha 16, é um objeto com os três métodos que declarei no módulo:

  • depositar: para enviar dinheiro para a conta

  • sacar: para remover dinheiro da conta

  • verSaldo: para ver o valor da conta no momento

Então da linha 19 em diante, eu apenas deposito e saco valores aleatórios. Também uso o console para saber como andam as contas.

O ponto principal é que o saldo da conta, que declarei na linha 2 é privado. Isso quer dizer que não é possível alterá-lo diretamente após iniciar a contaCorrente. Para isso, é preciso usar os métodos depositar e sacar.

O último caso de uso é mais raro, mas ainda assim é interessante conhecer.

Minificação

Minificar é remover tudo aquilo que "sobra" em um código.

Coisas que "sobram" geralmente são:

  • Espaços em branco

  • Quebras de linha

  • Variáveis com nomes longos

  • Ponto e vírgula desnecessários

  • Parênteses ou chaves desnecessários

  • Etc

Aqui, o foco é nas variáveis com nomes longos. Veja o código abaixo:

(function (w, d) {
  // corpo da IIFE
})(window, document);

Dentro do corpo da IIFE, a variável w representa window, e d representa document. Porém com menos caracteres.

É claro que esse exemplo é minúsculo e até dificulta a leitura do código. Porém algumas bibliotecas usam essa técnica para gerar um código final menor. Eles não usam esse código durante o desenvolvimento.

E aí, conseguiu ter uma noção do que é uma IIFE? 😬

Callback

Para usar uma função, você precisa declará-la e depois executá-la.

Você pode até executar uma função antes de declarar ela, por causa do hoisting .

O hoisting é como se o Javascript colocasse as declarações de funções no início do arquivo antes de começar a executar.

Assim, o Javascript salva elas na memória antes de tudo e não ocorre erro de Uncaught ReferenceError: myFunction is not defined.

Uma função invocada imediatamente faz essas duas etapas de uma vez.

Para criar uma IIFE, declare uma função normal dentro de um parênteses, e acrescente outro ao final. O segundo parênteses serve para enviar argumentos para a IIFE:

(function(mensagem) {
  console.log(mensagem);
})('Uma IIFE');

As principais vantagens de criar uma IIFE são:

  1. Evitar o conflito variáveis através do encapsulamento

  2. Manter a imutabilidade ao usar os parâmetros

Outras vantagens que você encontrar podem ser variações dessas duas.

Use uma expressão de função autoinvocada para:

  • Proteger variáveis do escopo global

  • Inicializar pequenos trechos da aplicação

  • Criar módulos

  • Minificar código

Você também pode chamar uma IIFE com os parênteses da execução dentro do primeiro. Não faz diferença. Vai a gosto do cliente:

// Opção que expliquei ao longo do artigo

(function() {
  var result = true;
  return result;
})(); // <<< aqui

// Outra opção

(function() {
  var result = true;
  return result;
}()); // <<< aqui

Obrigado pela sua leitura. Deixe um comentário aqui abaixo se ficou alguma dúvida. Será um prazer trocar uma ideia e te ajudar 😬

Continue estudando:

Veja outros posts sobre Front-end