Funções de ordem superior são um dos pilares da programação funcional em JavaScript. Elas permitem que funções sejam tratadas como valores, podendo ser atribuídas a variáveis, passadas como argumentos para outras funções e retornadas como resultado. Esse conceito é essencial para escrever código mais modular, reutilizável e expressivo.

Nesta aula, vamos mergulhar nos três aspectos principais: como funções são valores de primeira classe, o padrão de callbacks e a criação de funções a partir de funções. Ao final, você estará apto a usar e criar funções de ordem superior com confiança.

Funções como valores

Em JavaScript, funções são objetos de primeira classe. Isso significa que elas podem ser tratadas como qualquer outro valor: podem ser armazenadas em variáveis, arrays, objetos e passadas como argumentos. Essa característica é a base para as funções de ordem superior.

Por exemplo, podemos atribuir uma função a uma variável e depois invocá-la usando o nome da variável:

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

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

Também podemos armazenar funções em arrays ou objetos:

const operacoes = [
  function(a, b) { return a + b; },
  function(a, b) { return a - b; },
];

console.log(operacoes[0](5, 3)); // 8
console.log(operacoes[1](5, 3)); // 2

Essa flexibilidade permite que passemos funções como argumentos para outras funções, criando comportamentos personalizados.

Callbacks

Um callback é uma função passada como argumento para outra função, que será executada em algum momento futuro. Esse padrão é amplamente utilizado em operações assíncronas, como manipulação de eventos, requisições HTTP e leitura de arquivos, mas também é comum em funções síncronas para personalizar o comportamento.

Vamos criar uma função que recebe um array e um callback, e aplica o callback a cada elemento:

function processarArray(arr, callback) {
  for (let i = 0; i < arr.length; i++) {
    arr[i] = callback(arr[i]);
  }
  return arr;
}

const numeros = [1, 2, 3, 4];
const dobrados = processarArray(numeros, function(num) {
  return num * 2;
});

console.log(dobrados); // [2, 4, 6, 8]

Callbacks são fundamentais para funções como forEach, map, filter, reduce e outras. Por exemplo:

const frutas = ['maçã', 'banana', 'laranja'];
frutas.forEach(function(fruta, indice) {
  console.log(`${indice}: ${fruta}`);
});
// 0: maçã
// 1: banana
// 2: laranja

No contexto assíncrono, callbacks são usados para lidar com resultados após uma operação demorada, como em setTimeout:

setTimeout(function() {
  console.log('Executado após 2 segundos');
}, 2000);

Retornando funções

Funções de ordem superior também podem retornar outras funções. Isso permite criar funções com comportamentos configuráveis, como fábricas de funções ou closures que capturam variáveis do escopo pai.

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

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

const saudarBomDia = criarSaudador('Bom dia');
const saudarBoaNoite = criarSaudador('Boa noite');

console.log(saudarBomDia('João'));   // Bom dia, João!
console.log(saudarBoaNoite('Ana'));  // Boa noite, Ana!

Outro exemplo clássico é a função multiplicar que retorna um multiplicador:

function multiplicar(fator) {
  return function(numero) {
    return numero * fator;
  };
}

const dobrar = multiplicar(2);
const triplicar = multiplicar(3);

console.log(dobrar(5));    // 10
console.log(triplicar(5)); // 15

Essa técnica é poderosa para criar funções especializadas e evitar repetição de código. As funções retornadas mantêm acesso às variáveis do escopo da função externa, formando uma closure.

Boas práticas

Ao trabalhar com funções de ordem superior, prefira usar funções puras (sem efeitos colaterais) sempre que possível. Isso torna o código mais previsível e testável. Além disso, evite aninhamento excessivo de callbacks (callback hell) — para isso, use Promises ou async/await. Para callbacks síncronos, lembre-se de que a função de ordem superior é responsável por invocar o callback no momento adequado.

Referências

Exercícios

  1. Crie uma função de ordem superior chamada executarOperacao que recebe dois números e uma função callback. A função deve aplicar o callback aos dois números e retornar o resultado. Teste com uma função de soma e uma de multiplicação.

    ✓ Resposta:
    function executarOperacao(a, b, callback) {
      return callback(a, b);
    }
    
    const soma = (x, y) => x + y;
    const multiplicacao = (x, y) => x * y;
    
    console.log(executarOperacao(5, 3, soma)); // 8
    console.log(executarOperacao(5, 3, multiplicacao)); // 15
  2. Implemente sua própria versão da função forEach, chamada meuForEach, que recebe um array e um callback. O callback deve receber o elemento e o índice. Teste com um array de nomes.

    ✓ Resposta:
    function meuForEach(arr, callback) {
      for (let i = 0; i < arr.length; i++) {
        callback(arr[i], i);
      }
    }
    
    const nomes = ['Ana', 'Bruno', 'Carla'];
    meuForEach(nomes, function(nome, indice) {
      console.log(`${indice}: ${nome}`);
    });
    // 0: Ana
    // 1: Bruno
    // 2: Carla
  3. Crie uma função criarContador que retorna uma função que incrementa e retorna um contador interno. O contador deve começar em 0. Teste chamando a função retornada várias vezes.

    ✓ Resposta:
    function criarContador() {
      let contador = 0;
      return function() {
        contador++;
        return contador;
      };
    }
    
    const contador1 = criarContador();
    console.log(contador1()); // 1
    console.log(contador1()); // 2
    console.log(contador1()); // 3
    
    const contador2 = criarContador();
    console.log(contador2()); // 1 (independente)
  4. Escreva uma função de ordem superior chamada filtrar que recebe um array e um callback de teste (que retorna booleano). A função deve retornar um novo array com os elementos que passam no teste. Não use o método nativo filter.

    ✓ Resposta:
    function filtrar(arr, callback) {
      const resultado = [];
      for (let i = 0; i < arr.length; i++) {
        if (callback(arr[i], i, arr)) {
          resultado.push(arr[i]);
        }
      }
      return resultado;
    }
    
    const numeros = [1, 2, 3, 4, 5, 6];
    const pares = filtrar(numeros, function(num) {
      return num % 2 === 0;
    });
    console.log(pares); // [2, 4, 6]
  5. Crie uma função compor que recebe duas funções f e g e retorna uma nova função que aplica g ao argumento e depois f ao resultado (composição de funções). Exemplo: compor(dobrar, triplicar)(5) deve retornar dobrar(triplicar(5)) = 30.

    ✓ Resposta:
    function compor(f, g) {
      return function(x) {
        return f(g(x));
      };
    }
    
    const triplicar = x => x * 3;
    const dobrar = x => x * 2;
    
    const triplicarDepoisDobrar = compor(dobrar, triplicar);
    console.log(triplicarDepoisDobrar(5)); // 30 (dobrar(triplicar(5)) = 2 * (3*5) = 30)
    
    const dobrarDepoisTriplicar = compor(triplicar, dobrar);
    console.log(dobrarDepoisTriplicar(5)); // 30 (triplicar(dobrar(5)) = 3 * (2*5) = 30) - neste caso igual, mas geralmente não é comutativo