Funções de ordem superior são um conceito fundamental em JavaScript, permitindo que funções sejam tratadas como cidadãos de primeira classe. Isso significa que podemos atribuir funções a variáveis, passá-las como argumentos para outras funções e até mesmo retorná-las de outras funções. Esse paradigma é a base para padrões como callbacks, programação funcional e muitas APIs modernas, como métodos de array (map, filter, reduce).

Nesta aula, vamos explorar três aspectos essenciais: funções como valores, callbacks e retorno de funções. Você entenderá como esses conceitos se conectam e como aplicá-los no dia a dia para escrever código mais flexível e reutilizável.

Funções como valores

Em JavaScript, funções são objetos de primeira classe, o que significa que podem ser armazenadas em variáveis, propriedades de objetos, arrays e passadas como argumentos. Isso permite que você trate um bloco de código como um dado manipulável.

Por exemplo, você pode definir uma função anônima e atribuí-la a uma variável:

const saudacao = function(nome) {
  return `Olá, ${nome}!`;
};

console.log(saudacao('Maria')); // Olá, Maria!

Também é possível atribuir funções a propriedades de objetos ou colocá-las em arrays:

const operacoes = {
  soma: (a, b) => a + b,
  subtracao: (a, b) => a - b
};

console.log(operacoes.soma(5, 3)); // 8

const listaDeFuncoes = [
  x => x * 2,
  x => x + 1
];

console.log(listaDeFuncoes[0](10)); // 20

Essa capacidade de tratar funções como valores é o que possibilita o uso de funções de ordem superior.

Callbacks

Um callback é uma função passada como argumento para outra função, que será executada em um momento posterior, geralmente após a conclusão de uma operação assíncrona ou para personalizar o comportamento de uma função.

Um exemplo clássico é o método forEach de arrays, que recebe uma função callback para ser executada para cada elemento:

const numeros = [1, 2, 3];

numeros.forEach(function(numero) {
  console.log(numero * 2);
});
// Saída: 2, 4, 6

Você também pode criar suas próprias funções que aceitam callbacks. Por exemplo, uma função que executa uma operação em cada elemento de um array e retorna um novo array:

function transformar(array, callback) {
  const resultado = [];
  for (let i = 0; i < array.length; i++) {
    resultado.push(callback(array[i], i, array));
  }
  return resultado;
}

const dobrados = transformar([1, 2, 3], (num) => num * 2);
console.log(dobrados); // [2, 4, 6]

Callbacks são essenciais para programação assíncrona, como em requisições AJAX, leitura de arquivos e temporizadores:

setTimeout(() => {
  console.log('Executado após 1 segundo');
}, 1000);

No entanto, o uso excessivo de callbacks pode levar ao "callback hell". Para mitigar isso, JavaScript oferece Promises e async/await, mas o conceito de callback continua sendo fundamental.

Retornando funções

Uma função pode retornar outra função, criando closures e permitindo a criação de funções especializadas ou fábricas de funções. Isso é útil para criar funções com comportamento configurável.

Exemplo simples: uma função que retorna um saudador personalizado:

function criarSaudador(saudacao) {
  return function(nome) {
    console.log(`${saudacao}, ${nome}!`);
  };
}

const saudarOla = criarSaudador('Olá');
saudarOla('João'); // Olá, João!

const saudarOi = criarSaudador('Oi');
saudarOi('Maria'); // Oi, Maria!

Outro exemplo clássico é a função bind ou a criação de funções com argumentos pré-preenchidos (currying):

function multiplicar(a) {
  return function(b) {
    return a * b;
  };
}

const dobrar = multiplicar(2);
console.log(dobrar(5)); // 10

const triplicar = multiplicar(3);
console.log(triplicar(5)); // 15

Essa técnica é amplamente utilizada em bibliotecas como React (hooks personalizados) e Redux (middleware).

Boas práticas e observações finais

Ao trabalhar com funções de ordem superior, lembre-se de:

  • Usar nomes descritivos para callbacks e funções retornadas.
  • Evitar callbacks aninhados profundamente; prefira Promises ou async/await.
  • Documentar o tipo de callback esperado (parâmetros e retorno).
  • Tomar cuidado com o escopo e o valor de this; use arrow functions quando apropriado.
  • Testar funções de ordem superior com diferentes entradas para garantir robustez.

Funções de ordem superior são uma ferramenta poderosa que torna o código mais expressivo e modular. Dominá-las é essencial para se tornar um desenvolvedor JavaScript proficiente.

Referências

Exercícios

  1. Crie uma função de ordem superior chamada executarVariasVezes que recebe uma função fn e um número n, e executa fn n vezes. Teste com uma função que imprime "Olá".

    ✓ Resposta:
    function executarVariasVezes(fn, n) {
      for (let i = 0; i < n; i++) {
        fn();
      }
    }
    
    executarVariasVezes(() => console.log('Olá'), 3); // Olá (3 vezes)
  2. Implemente uma função filtrar que recebe um array e uma função callback de teste, e retorna um novo array apenas com os elementos que passam no teste. Não use filter nativo.

    ✓ Resposta:
    function filtrar(array, callback) {
      const resultado = [];
      for (let i = 0; i < array.length; i++) {
        if (callback(array[i], i, array)) {
          resultado.push(array[i]);
        }
      }
      return resultado;
    }
    
    const numeros = [1, 2, 3, 4, 5];
    const pares = filtrar(numeros, num => num % 2 === 0);
    console.log(pares); // [2, 4]
  3. Crie uma função compor que recebe duas funções f e g e retorna uma nova função que aplica g a um argumento e depois f ao resultado. Por exemplo, compor(x => x * 2, x => x + 1)(5) deve retornar 12.

    ✓ Resposta:
    function compor(f, g) {
      return function(x) {
        return f(g(x));
      };
    }
    
    const multiplicarPor2 = x => x * 2;
    const somar1 = x => x + 1;
    const funcaoComposta = compor(multiplicarPor2, somar1);
    console.log(funcaoComposta(5)); // 12 (primeiro soma 1, depois multiplica por 2)
  4. Escreva uma função criarContador que retorna um objeto com dois métodos: incrementar e valor. O contador deve começar em 0 e ser privado (usando closure).

    ✓ Resposta:
    function criarContador() {
      let contador = 0;
      return {
        incrementar: function() {
          contador++;
        },
        valor: function() {
          return contador;
        }
      };
    }
    
    const meuContador = criarContador();
    meuContador.incrementar();
    meuContador.incrementar();
    console.log(meuContador.valor()); // 2
  5. Implemente uma função mapAsync que recebe um array e uma função callback assíncrona (que retorna uma Promise) e retorna uma Promise que resolve com um array dos resultados. Use Promise.all.

    ✓ Resposta:
    function mapAsync(array, callback) {
      return Promise.all(array.map(callback));
    }
    
    // Exemplo:
    const numeros = [1, 2, 3];
    const asyncDobrar = (num) => Promise.resolve(num * 2);
    mapAsync(numeros, asyncDobrar).then(resultado => {
      console.log(resultado); // [2, 4, 6]
    });