Funções Avançadas em JavaScript

Até agora, você aprendeu sobre funções tradicionais em JavaScript - como declará-las, passar parâmetros e retornar valores. Agora, vamos explorar formas mais modernas e flexíveis de trabalhar com funções: as arrow functions e o conceito de callbacks. Estes conceitos expandirão significativamente suas possibilidades como programador.

Arrow Functions: Uma Sintaxe Mais Elegante

O Que São Arrow Functions?

Arrow functions (funções seta) são uma forma mais concisa de escrever funções em JavaScript. Elas foram introduzidas no ES6 (ECMAScript 2015) e rapidamente se tornaram populares pela sua sintaxe limpa e clara.

Sintaxe Básica

Vamos comparar uma função tradicional com sua versão arrow function:

// Função tradicional
function somar(a, b) {
    return a + b;
}

// Arrow function equivalente
const somar = (a, b) => {
    return a + b;
};

// Arrow function concisa (sem chaves e return implícito)
const somar = (a, b) => a + b;

Observe as diferenças:

  • Não usamos a palavra-chave function
  • Usamos uma "seta" => entre os parâmetros e o corpo da função
  • Se a função tem apenas uma expressão, podemos omitir as chaves {} e o return

Casos Especiais de Sintaxe

Um Único Parâmetro

Quando a função tem apenas um parâmetro, podemos omitir os parênteses:

// Com parênteses
const dobrar = (numero) => numero * 2;

// Sem parênteses (mais conciso)
const dobrar = numero => numero * 2;

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

Sem Parâmetros

Se a função não tem parâmetros, os parênteses vazios são obrigatórios:

const saudacao = () => "Olá, mundo!";

console.log(saudacao()); // "Olá, mundo!"

Retornando Objetos

Para retornar um objeto literal diretamente, envolva-o em parênteses:

// Incorreto - JavaScript confunde as chaves do objeto com as chaves da função
const criarPessoa = nome => { nome: nome }; // ❌ Erro!

// Correto - usando parênteses
const criarPessoa = nome => ({ nome: nome });

console.log(criarPessoa("Maria")); // { nome: "Maria" }

Arrow Functions Multilinha

Quando precisamos de múltiplas instruções, usamos chaves e o return explícito:

const calcularMedia = (numeros) => {
    let soma = 0;
    
    for (let numero of numeros) {
        soma += numero;
    }
    
    return soma / numeros.length;
};

const notas = [7.5, 8.0, 6.5, 9.0];
console.log(calcularMedia(notas)); // 7.75

Quando Usar Arrow Functions?

Use arrow functions quando:

  • A função é simples e curta
  • A função é passada como callback (veremos a seguir)
  • Você quer código mais conciso e legível

Use funções tradicionais quando:

  • A função é complexa e longa
  • Você precisa de comportamentos especiais do this (conceito avançado)
  • A função precisa ter um nome claro para facilitar depuração

Funções Como Valores: Cidadãos de Primeira Classe

Conceito Fundamental

Uma das características mais poderosas do JavaScript é que funções são valores. Isso significa que podemos:

  • Atribuir funções a variáveis
  • Passar funções como argumentos para outras funções
  • Retornar funções de outras funções
  • Armazenar funções em arrays ou objetos

Esta característica faz com que funções sejam "cidadãos de primeira classe" (first-class citizens) em JavaScript.

Exemplos Práticos

// 1. Funções em variáveis
const saudacao = function(nome) {
    return `Olá, ${nome}!`;
};

// 2. Funções em arrays
const operacoes = [
    (x) => x + 10,
    (x) => x * 2,
    (x) => x - 5
];

let valor = 5;
for (let operacao of operacoes) {
    valor = operacao(valor);
    console.log(valor); // 15, 30, 25
}

// 3. Funções em objetos
const calculadora = {
    somar: (a, b) => a + b,
    subtrair: (a, b) => a - b,
    multiplicar: (a, b) => a * b
};

console.log(calculadora.somar(5, 3)); // 8

Callbacks: Funções Que Esperam Sua Vez

O Que São Callbacks?

Um callback é uma função que você passa como argumento para outra função, esperando que ela seja executada ("chamada de volta") em algum momento específico.

É como dar instruções a alguém dizendo: "Quando você terminar isso, faça aquilo que eu te disser".

Exemplo Simples

function processarNumero(numero, operacao) {
    console.log("Processando o número...");
    const resultado = operacao(numero);
    console.log("Resultado:", resultado);
    return resultado;
}

// Usando diferentes callbacks
processarNumero(5, numero => numero * 2);
// Processando o número...
// Resultado: 10

processarNumero(5, numero => numero + 100);
// Processando o número...
// Resultado: 105

Callbacks com Arrays

Os métodos de array que você aprendeu anteriormente usam callbacks extensivamente:

const numeros = [1, 2, 3, 4, 5];

// forEach - executa um callback para cada elemento
numeros.forEach(numero => {
    console.log(`O número é: ${numero}`);
});

// map - transforma cada elemento usando o callback
const dobrados = numeros.map(numero => numero * 2);
console.log(dobrados); // [2, 4, 6, 8, 10]

// filter - mantém elementos que passam no teste do callback
const pares = numeros.filter(numero => numero % 2 === 0);
console.log(pares); // [2, 4]

// find - retorna o primeiro elemento que passa no teste
const maiorQueTres = numeros.find(numero => numero > 3);
console.log(maiorQueTres); // 4

Callback Personalizado - Exemplo Prático

Vamos criar uma função que processa uma lista de tarefas:

function processarTarefas(tarefas, callback) {
    console.log("Iniciando processamento...");
    
    for (let tarefa of tarefas) {
        callback(tarefa);
    }
    
    console.log("Processamento concluído!");
}

const minhasTarefas = [
    "Estudar JavaScript",
    "Fazer exercícios",
    "Revisar código"
];

// Usando diferentes callbacks
processarTarefas(minhasTarefas, tarefa => {
    console.log(`✓ ${tarefa}`);
});

processarTarefas(minhasTarefas, tarefa => {
    console.log(`Tarefa: ${tarefa} - Concluída!`);
});

Callbacks com Múltiplos Parâmetros

Callbacks podem receber múltiplos argumentos:

function processarProdutos(produtos, callback) {
    for (let i = 0; i < produtos.length; i++) {
        // Passa o produto e seu índice para o callback
        callback(produtos[i], i);
    }
}

const produtos = ["Notebook", "Mouse", "Teclado"];

processarProdutos(produtos, (produto, indice) => {
    console.log(`${indice + 1}. ${produto}`);
});
// 1. Notebook
// 2. Mouse
// 3. Teclado

Higher-Order Functions: Funções que Trabalham com Funções

O Que São?

Higher-order functions (funções de ordem superior) são funções que:

  • Recebem outras funções como argumentos, OU
  • Retornam funções como resultado

Funções que Recebem Funções

Você já viu exemplos com callbacks. Aqui está outro exemplo prático:

function aplicarOperacao(array, operacao) {
    const resultado = [];
    
    for (let item of array) {
        resultado.push(operacao(item));
    }
    
    return resultado;
}

const numeros = [1, 2, 3, 4, 5];

// Diferentes operações
const quadrados = aplicarOperacao(numeros, x => x * x);
console.log(quadrados); // [1, 4, 9, 16, 25]

const incrementados = aplicarOperacao(numeros, x => x + 10);
console.log(incrementados); // [11, 12, 13, 14, 15]

Funções que Retornam Funções

Uma função pode criar e retornar outra função:

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

// Criando funções especializadas
const duplicar = criarMultiplicador(2);
const triplicar = criarMultiplicador(3);
const quintuplicar = criarMultiplicador(5);

console.log(duplicar(10));      // 20
console.log(triplicar(10));     // 30
console.log(quintuplicar(10));  // 50

Por que isso é útil?

  • Você cria funções personalizadas dinamicamente
  • Evita repetição de código
  • Torna o código mais flexível e reutilizável

Exemplo Prático Completo

function criarValidador(tipoValidacao) {
    if (tipoValidacao === "email") {
        return (texto) => texto.includes("@") && texto.includes(".");
    }
    
    if (tipoValidacao === "telefone") {
        return (texto) => texto.length >= 10 && /^\d+$/.test(texto);
    }
    
    if (tipoValidacao === "naoVazio") {
        return (texto) => texto.trim().length > 0;
    }
    
    return () => true; // Validador padrão que sempre passa
}

// Criando validadores específicos
const validarEmail = criarValidador("email");
const validarTelefone = criarValidador("telefone");
const validarNome = criarValidador("naoVazio");

console.log(validarEmail("teste@email.com"));  // true
console.log(validarEmail("invalido"));         // false
console.log(validarTelefone("1234567890"));    // true
console.log(validarTelefone("12-345"));        // false

Closures: Memória das Funções

O Que São Closures?

Um closure ocorre quando uma função "lembra" das variáveis do escopo onde foi criada, mesmo depois de sair desse escopo.

É como se a função levasse uma "mochila" com as variáveis que ela precisa.

Exemplo Básico

function criarContador() {
    let contador = 0; // Variável "privada"
    
    return function() {
        contador++;
        return contador;
    };
}

const meuContador = criarContador();

console.log(meuContador()); // 1
console.log(meuContador()); // 2
console.log(meuContador()); // 3

// Criando outro contador independente
const outroContador = criarContador();
console.log(outroContador()); // 1
console.log(outroContador()); // 2

O que aconteceu?

  • A função retornada "lembra" da variável contador
  • Cada vez que criamos um novo contador, ele tem seu próprio contador independente
  • A variável contador não pode ser acessada diretamente de fora

Exemplo Prático: Configurador de Mensagens

function criarFormatadorMensagem(prefixo) {
    return function(mensagem) {
        return `${prefixo}: ${mensagem}`;
    };
}

const logInfo = criarFormatadorMensagem("[INFO]");
const logErro = criarFormatadorMensagem("[ERRO]");
const logAviso = criarFormatadorMensagem("[AVISO]");

console.log(logInfo("Sistema iniciado"));      // [INFO]: Sistema iniciado
console.log(logErro("Falha na conexão"));      // [ERRO]: Falha na conexão
console.log(logAviso("Memória em 80%"));       // [AVISO]: Memória em 80%

Closure com Múltiplas Funções

function criarCarteira(saldoInicial) {
    let saldo = saldoInicial;
    
    return {
        depositar: function(valor) {
            saldo += valor;
            return `Depósito de R$ ${valor}. Saldo atual: R$ ${saldo}`;
        },
        
        sacar: function(valor) {
            if (valor > saldo) {
                return "Saldo insuficiente!";
            }
            saldo -= valor;
            return `Saque de R$ ${valor}. Saldo atual: R$ ${saldo}`;
        },
        
        consultarSaldo: function() {
            return `Saldo atual: R$ ${saldo}`;
        }
    };
}

const minhaCarteira = criarCarteira(100);

console.log(minhaCarteira.consultarSaldo()); // Saldo atual: R$ 100
console.log(minhaCarteira.depositar(50));    // Depósito de R$ 50. Saldo atual: R$ 150
console.log(minhaCarteira.sacar(30));        // Saque de R$ 30. Saldo atual: R$ 120
console.log(minhaCarteira.sacar(200));       // Saldo insuficiente!

Diferenças Entre Tipos de Função

Comparação Rápida

| Aspecto | Função Declarada | Expressão de Função | Arrow Function | |---------|------------------|---------------------|----------------| | Sintaxe | function nome() {} | const nome = function() {} | const nome = () => {} | | Hoisting | Sim | Não | Não | | Nome | Sempre tem | Pode ser anônima | Sempre anônima | | Uso como método | ✓ | ✓ | Limitado | | Concisão | Moderada | Moderada | Alta | | Contexto this | Próprio | Próprio | Herda do pai |

Quando Usar Cada Uma?

Função Declarada:

function calcularTotal(itens) {
    // Boa para funções principais e nomeadas
    // Pode ser chamada antes da definição (hoisting)
}

Expressão de Função:

const calcularDesconto = function(valor, percentual) {
    // Boa quando você quer controlar quando a função é definida
    // Útil em estruturas condicionais
};

Arrow Function:

const aplicarDesconto = (valor, desconto) => valor * (1 - desconto);
// Ótima para callbacks e funções curtas
// Sintaxe mais limpa e moderna

Exemplos Práticos Integrados

Sistema de Processamento de Pedidos

function criarSistemaPedidos() {
    const pedidos = [];
    
    return {
        adicionar: (pedido) => {
            pedidos.push({
                ...pedido,
                id: pedidos.length + 1,
                data: new Date()
            });
            return "Pedido adicionado!";
        },
        
        listar: () => pedidos,
        
        buscarPor: (criterio) => {
            return pedidos.filter(criterio);
        },
        
        processar: (callback) => {
            pedidos.forEach((pedido, indice) => {
                callback(pedido, indice);
            });
        }
    };
}

const sistema = criarSistemaPedidos();

// Adicionando pedidos
sistema.adicionar({ produto: "Notebook", valor: 3000 });
sistema.adicionar({ produto: "Mouse", valor: 50 });
sistema.adicionar({ produto: "Teclado", valor: 200 });

// Buscando pedidos caros (usando callback)
const pedidosCaros = sistema.buscarPor(pedido => pedido.valor > 100);
console.log(pedidosCaros);

// Processando todos os pedidos (usando callback)
sistema.processar((pedido, indice) => {
    console.log(`${indice + 1}. ${pedido.produto} - R$ ${pedido.valor}`);
});

Sistema de Filtros de Produtos

function criarFiltrosProdutos(produtos) {
    return {
        porPreco: (min, max) => {
            return produtos.filter(p => p.preco >= min && p.preco <= max);
        },
        
        porCategoria: (categoria) => {
            return produtos.filter(p => p.categoria === categoria);
        },
        
        comDesconto: () => {
            return produtos.filter(p => p.desconto > 0);
        },
        
        ordenar: (criterio) => {
            const copia = [...produtos];
            return copia.sort(criterio);
        }
    };
}

const produtos = [
    { nome: "Notebook", preco: 3000, categoria: "Eletrônicos", desconto: 10 },
    { nome: "Cadeira", preco: 500, categoria: "Móveis", desconto: 0 },
    { nome: "Mouse", preco: 50, categoria: "Eletrônicos", desconto: 5 }
];

const filtros = criarFiltrosProdutos(produtos);

console.log(filtros.porPreco(100, 1000));
console.log(filtros.porCategoria("Eletrônicos"));
console.log(filtros.comDesconto());
console.log(filtros.ordenar((a, b) => a.preco - b.preco));

Próximmos passos

Na próxima aula nos aprofundaremos no universo dos Arrays. Você aprenderá:

  • Como criar e usar arrays
  • Sobre métodos e propriedades importantes
  • Como percorrer por todos os dados de um array
  • A mutabilidade dos Arrays

Continue praticando! A compreensão profunda desses conceitos virá com a aplicação prática em diferentes situações.