0%
JavaScript Moderno: Funções

Funções

As funções modernas em JavaScript

10-15 min

JavaScript Moderno: Funções Avançadas

As funções estão no coração do JavaScript e são um dos seus aspectos mais poderosos. Neste tutorial, vamos explorar os recursos modernos das funções em JavaScript, em especial as arrow functions (funções de seta), as closures, as funções de ordem superior e os padrões funcionais essenciais.

O JavaScript costuma ser descrito como uma linguagem com “funções de primeira classe”, o que significa que as funções são tratadas como qualquer outra variável. Essa característica proporciona grande flexibilidade e permite padrões de programação poderosos.

Revisão dos fundamentos das funções

Antes de explorar os conceitos avançados, vamos relembrar as diferentes formas de definir funções em JavaScript.

Declaração de função

function saluer(nom) {
    return `Bonjour, ${nom} !`;
}

console.log(saluer("Marie")); // "Bonjour, Marie !"

Expressão de função

const saluer = function(nom) {
    return `Bonjour, ${nom} !`;
};

console.log(saluer("Pierre")); // "Bonjour, Pierre !"

Arrow function (ES6)

const saluer = (nom) => {
    return `Bonjour, ${nom} !`;
};

// Versão concisa para funções simples
const saluerConcis = nom => `Bonjour, ${nom} !`;

console.log(saluerConcis("Jean")); // "Bonjour, Jean !"

Arrow Functions (funções de seta)

As arrow functions, introduzidas com o ES6, trazem uma sintaxe concisa e comportamentos específicos que as tornam particularmente úteis em determinados contextos.

Sintaxe simplificada

// Função padrão
const double = function(x) {
    return x * 2;
};

// Arrow function equivalente
const doubleArrow = (x) => {
    return x * 2;
};

// Versão ultraconcisa (sem chaves nem return explícito)
const doubleConc = x => x * 2;

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

Parênteses e parâmetros

// Sem parâmetro - parênteses obrigatórios
const salut = () => "Bonjour !";

// Um único parâmetro - parênteses opcionais
const carre = x => x * x;

// Vários parâmetros - parênteses obrigatórios
const somme = (a, b) => a + b;

// Retorno de um objeto - parênteses necessários ao redor do objeto
const creerPersonne = (nom, age) => ({ nom, age });
console.log(creerPersonne("Alice", 30)); // { nom: "Alice", age: 30 }

Comportamento de this

A principal diferença funcional entre as arrow functions e as funções tradicionais é a forma como lidam com a palavra-chave this.

// Em uma função tradicional, this depende do contexto de chamada
function FonctionClassique() {
    this.valeur = 42;
    setTimeout(function() {
        console.log(this.valeur); // undefined (this se refere ao objeto global)
    }, 1000);
}

// Em uma arrow function, this é léxico (herdado do contexto pai)
function FonctionAvecFleche() {
    this.valeur = 42;
    setTimeout(() => {
        console.log(this.valeur); // 42 (this é o de FonctionAvecFleche)
    }, 1000);
}

Quando usar arrow functions

As arrow functions são ideais para:

  • Funções curtas e simples
  • Callbacks, particularmente em métodos de array (map, filter, etc.)
  • Funções anônimas
  • Casos em que você quer preservar o contexto this

Elas não são adequadas para:

  • Métodos de objeto (que devem se referir ao objeto com this)
  • Funções construtoras (não há new com arrow functions)
  • Funções que usam arguments (as arrow functions não têm o objeto arguments)
// Exemplo de uso com métodos de array
const nombres = [1, 2, 3, 4, 5];
const carres = nombres.map(x => x * x); // [1, 4, 9, 16, 25]

// Não usar em métodos de objeto
const objet = {
    valeur: 42,
    // Abordagem ruim
    mauvaiseFonction: () => {
        console.log(this.valeur); // undefined, this é o objeto global
    },
    // Abordagem boa
    bonneFonction() {
        console.log(this.valeur); // 42
    }
};

Parâmetros de função avançados

O ES6 e as versões seguintes introduziram vários recursos para lidar com parâmetros de maneira mais flexível e expressiva.

Parâmetros padrão

function saluer(nom = "Utilisateur", message = "Bonjour") {
    return `${message}, ${nom} !`;
}

console.log(saluer()); // "Bonjour, Utilisateur !"
console.log(saluer("Marie")); // "Bonjour, Marie !"
console.log(saluer("Pierre", "Bienvenue")); // "Bienvenue, Pierre !"

// Expressões como valores padrão
function getDateFormatee(date = new Date()) {
    return date.toLocaleDateString();
}

// Usar parâmetros anteriores como valores padrão
function calculer(a = 1, b = a * 2) {
    return a + b;
}
console.log(calculer()); // 3 (1 + 2)

Parâmetros rest (Rest Parameters)

function somme(...nombres) {
    return nombres.reduce((total, n) => total + n, 0);
}

console.log(somme(1, 2, 3, 4, 5)); // 15

// Combinação com parâmetros normais
function formatMessage(prefix, suffix, ...messages) {
    return messages.map(msg => `${prefix}${msg}${suffix}`);
}

console.log(formatMessage("<<", ">>", "Info", "Erreur", "Avertissement"));
// ["<<Info>>", "<<Erreur>>", "<<Avertissement>>"]

Desestruturação de parâmetros

// Desestruturação de objeto nos parâmetros
function afficherUtilisateur({ nom, age, email = "Non fourni" }) {
    console.log(`Nom: ${nom}, Age: ${age}, Email: ${email}`);
}

afficherUtilisateur({ nom: "Alice", age: 30 });
// "Nom: Alice, Age: 30, Email: Non fourni"

// Desestruturação de array
function afficherCoordonnees([x, y, z = 0]) {
    console.log(`X: ${x}, Y: ${y}, Z: ${z}`);
}

afficherCoordonnees([10, 20]);
// "X: 10, Y: 20, Z: 0"

Closures (Fechamentos)

As closures são um dos conceitos mais poderosos do JavaScript: permitem que uma função “se lembre” do seu ambiente léxico mesmo depois que a função pai terminou sua execução.

Entendendo as closures

function creerCompteur() {
    let compte = 0; // Variável privada
    
    return function() {
        compte++; // Acesso à variável do escopo pai
        return compte;
    };
}

const compteur = creerCompteur();
console.log(compteur()); // 1
console.log(compteur()); // 2
console.log(compteur()); // 3

// A variável compte está "encapsulada" na closure
// e é inacessível do lado de fora

Aplicações práticas das closures

Encapsulamento de dados

function creerBanque(soldeInitial) {
    let solde = soldeInitial; // Variável privada
    
    return {
        deposer(montant) {
            solde += montant;
            return solde;
        },
        retirer(montant) {
            if (montant <= solde) {
                solde -= montant;
                return true;
            }
            return false;
        },
        getSolde() {
            return solde;
        }
    };
}

const monCompte = creerBanque(100);
console.log(monCompte.getSolde()); // 100
monCompte.deposer(50);
console.log(monCompte.getSolde()); // 150
monCompte.retirer(30);
console.log(monCompte.getSolde()); // 120
// solde é inacessível diretamente: console.log(monCompte.solde) // undefined

Criação de funções personalizadas

function multiplierPar(facteur) {
    return function(nombre) {
        return nombre * facteur;
    };
}

const double = multiplierPar(2);
const triple = multiplierPar(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Gerenciamento de contexto assíncrono

function fetchData(url) {
    return function(callback) {
        fetch(url)
            .then(response => response.json())
            .then(data => callback(data))
            .catch(error => console.error(error));
    };
}

const getUserData = fetchData('https://api.example.com/users');
// Mais tarde no código
getUserData(data => {
    console.log("Données reçues:", data);
});

O problema clássico de loop e closure

Um problema comum com closures surge ao usar variáveis de loop em funções assíncronas:

// Problema
function probleme() {
    const fonctions = [];
    
    for (var i = 0; i < 3; i++) {
        fonctions.push(function() {
            console.log(i);
        });
    }
    
    // Neste ponto, i passou a valer 3
    return fonctions;
}

const fns = probleme();
fns[0](); // 3 (e não 0, como se poderia esperar)
fns[1](); // 3
fns[2](); // 3

// Solução com let (escopo de bloco)
function solution() {
    const fonctions = [];
    
    for (let i = 0; i < 3; i++) {
        // Cada iteração cria um novo i
        fonctions.push(function() {
            console.log(i);
        });
    }
    
    return fonctions;
}

const fnsSolution = solution();
fnsSolution[0](); // 0
fnsSolution[1](); // 1
fnsSolution[2](); // 2

// Solução com closure explícita (para var)
function solutionClassique() {
    const fonctions = [];
    
    for (var i = 0; i < 3; i++) {
        // IIFE (Immediately Invoked Function Expression)
        (function(index) {
            fonctions.push(function() {
                console.log(index);
            });
        })(i);
    }
    
    return fonctions;
}

Funções de ordem superior

Funções de ordem superior são funções que recebem outras funções como argumentos ou que retornam funções. Elas são fundamentais para a programação funcional em JavaScript.

Funções que aceitam callbacks

// Função de ordem superior que recebe uma função como parâmetro
function appliquerOperation(nombre, operation) {
    return operation(nombre);
}

// Funções que podem ser usadas como callbacks
const double = x => x * 2;
const carre = x => x * x;
const absolu = x => Math.abs(x);

console.log(appliquerOperation(5, double)); // 10
console.log(appliquerOperation(-3, absolu)); // 3
console.log(appliquerOperation(4, carre)); // 16

Funções que retornam funções

// Currying - transformar uma função com vários argumentos
// em uma sequência de funções de um único argumento
function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn(...args);
        } else {
            return function(...args2) {
                return curried(...args, ...args2);
            };
        }
    };
}

function ajouter(a, b, c) {
    return a + b + c;
}

const ajouterCurry = curry(ajouter);
console.log(ajouterCurry(1)(2)(3)); // 6
console.log(ajouterCurry(1, 2)(3)); // 6
console.log(ajouterCurry(1)(2, 3)); // 6

// Composição de funções
const compose = (f, g) => x => f(g(x));

const ajouterUn = x => x + 1;
const multiplierParDeux = x => x * 2;

const ajouterPuisMultiplier = compose(multiplierParDeux, ajouterUn);
console.log(ajouterPuisMultiplier(5)); // (5 + 1) * 2 = 12

Exemplos de funções de ordem superior nativas

O JavaScript possui vários métodos de ordem superior nativos para arrays:

const nombres = [1, 2, 3, 4, 5, 6];

// map - transforma cada elemento
const doubles = nombres.map(n => n * 2); // [2, 4, 6, 8, 10, 12]

// filter - seleciona alguns elementos
const pairs = nombres.filter(n => n % 2 === 0); // [2, 4, 6]

// reduce - acumula os valores
const somme = nombres.reduce((acc, n) => acc + n, 0); // 21

// find - encontra o primeiro elemento que satisfaz uma condição
const premierPair = nombres.find(n => n % 2 === 0); // 2

// every - verifica se todos os elementos satisfazem uma condição
const tousSontPositifs = nombres.every(n => n > 0); // true

// some - verifica se pelo menos um elemento satisfaz uma condição
const auMoinsUnGrand = nombres.some(n => n > 4); // true

Padrões funcionais avançados

O JavaScript permite implementar vários padrões oriundos da programação funcional.

Função pura

Uma função pura não tem efeitos colaterais e sempre retorna o mesmo resultado para as mesmas entradas:

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

// Função impura (efeito colateral)
let total = 0;
function ajouterAuTotal(valeur) {
    total += valeur; // Modifica um estado externo
    return total;
}

Imutabilidade

A imutabilidade consiste em nunca modificar os dados, mas criar novas versões deles:

// Abordagem imutável
const array1 = [1, 2, 3];
const array2 = [...array1, 4]; // Cria um novo array
// array1 continua [1, 2, 3]

const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // Novo objeto
// obj1 continua { a: 1, b: 2 }

Memoization

A memoização é uma técnica de otimização que consiste em armazenar em cache os resultados das chamadas de função:

function memoize(fn) {
    const cache = new Map();
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (cache.has(key)) {
            return cache.get(key);
        }
        
        const result = fn(...args);
        cache.set(key, result);
        return result;
    };
}

// Função recursiva custosa
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Versão otimizada com memoization
const fiboMemo = memoize(function(n) {
    if (n <= 1) return n;
    return fiboMemo(n - 1) + fiboMemo(n - 2);
});

console.time("sans memo");
console.log(fibonacci(35)); // Muito lento
console.timeEnd("sans memo");

console.time("avec memo");
console.log(fiboMemo(35)); // Muito mais rápido
console.timeEnd("avec memo");

Contexto de execução e this

O gerenciamento do contexto this é crucial em JavaScript e fonte de muita confusão.

Controle do contexto this

O JavaScript fornece vários métodos para controlar o contexto de execução:

const personne = {
    nom: "Alice",
    saluer() {
        return `Bonjour, je suis ${this.nom}`;
    }
};

// Abordagem normal
console.log(personne.saluer()); // "Bonjour, je suis Alice"

// Perda de contexto
const fonctionSaluer = personne.saluer;
console.log(fonctionSaluer()); // "Bonjour, je suis undefined"

// Solução 1: bind
const fonctionLiee = personne.saluer.bind(personne);
console.log(fonctionLiee()); // "Bonjour, je suis Alice"

// Solução 2: call
console.log(personne.saluer.call({ nom: "Bob" })); // "Bonjour, je suis Bob"

// Solução 3: apply (como call, mas recebe um array de argumentos)
console.log(personne.saluer.apply({ nom: "Charlie" })); // "Bonjour, je suis Charlie"

Diferenças entre call, apply e bind

function presenter(prefix, suffix) {
    return `${prefix} ${this.nom} ${suffix}`;
}

const user = { nom: "Alice" };

// call: invoca imediatamente com argumentos separados
console.log(presenter.call(user, "Bonjour", "!")); // "Bonjour Alice !"

// apply: invoca imediatamente com argumentos em um array
console.log(presenter.apply(user, ["Salut", "..."])); // "Salut Alice ..."

// bind: cria uma nova função vinculada ao contexto, sem invocá-la
const presenterUtilisateur = presenter.bind(user, "Bienvenue");
console.log(presenterUtilisateur("!")); // "Bienvenue Alice !"

Funções autoexecutáveis e IIFE

IIFE (Immediately Invoked Function Expression)

As IIFE são funções que são executadas imediatamente após sua definição:

// Sintaxe clássica
(function() {
    const message = "Je m'exécute immédiatement";
    console.log(message);
})();

// Com parâmetros
(function(nom) {
    console.log(`Bonjour, ${nom} !`);
})("Alice");

// Com arrow functions
(() => {
    console.log("IIFE avec flèche");
})();

Utilidades das IIFE

  1. Isolar variáveis: evitar poluir o escopo global
  2. Encapsular dados: criar módulos simples
  3. Executar código imediatamente: inicializar aplicações
  4. Criar closures: capturar valores do ambiente
// Exemplo de módulo com IIFE
const compteur = (function() {
    let compte = 0; // Variável privada
    
    return {
        incrementer() {
            return ++compte;
        },
        decrementer() {
            return --compte;
        },
        valeur() {
            return compte;
        }
    };
})();

console.log(compteur.incrementer()); // 1
console.log(compteur.incrementer()); // 2
console.log(compteur.decrementer()); // 1

Funções geradoras e async/await

As funções geradoras e o async/await são mecanismos poderosos para gerenciar fluxos de execução complexos.

Funções geradoras

As funções geradoras podem suspender e retomar sua execução:

function* compterJusqua(n) {
    for (let i = 1; i <= n; i++) {
        yield i; // suspende a execução e retorna o valor
    }
}

const compteur = compterJusqua(5);
console.log(compteur.next().value); // 1
console.log(compteur.next().value); // 2
console.log(compteur.next().value); // 3
console.log(compteur.next().value); // 4
console.log(compteur.next().value); // 5
console.log(compteur.next().value); // undefined

// Iteração com for...of
for (const valeur of compterJusqua(3)) {
    console.log(valeur); // 1, 2, 3
}

Funções async e await

As funções async/await simplificam o gerenciamento de código assíncrono:

// Função que retorna uma Promise
function attendre(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// Função assíncrona usando await
async function processAsync() {
    console.log("Début");
    
    await attendre(1000);
    console.log("Après 1 seconde");
    
    await attendre(1000);
    console.log("Après 2 secondes");
    
    return "Terminé";
}

// Utilização
processAsync().then(resultat => {
    console.log(resultat); // "Terminé" após 2 segundos
});

// Tratamento de erros
async function fetchData(url) {
    try {
        const response = await fetch(url);
        const data = await response.json();
        return data;
    } catch (error) {
        console.error("Erreur lors de la récupération:", error);
        throw error; // relança o erro para tratamento posterior
    }
}

Exercícios práticos

Para solidificar seus conhecimentos, experimente estes exercícios:

  1. Filtragem avançada: escreva uma função de ordem superior filtrerPar que receba um critério (uma função) e retorne uma função que filtre um array de acordo com esse critério.

  2. Debounce: crie uma função debounce que limite a frequência de execução de uma função. Útil para manipuladores de eventos, como a busca em tempo real.

  3. Pipeline: implemente uma função pipeline que receba várias funções e as execute em sequência, passando o resultado de cada função para a seguinte.

  4. Memoization personalizada: melhore a função memoize para levar em conta uma função de geração de chave personalizada.

  5. Gerenciador de promises: crie uma função que receba um array de promises e retorne uma nova promise que seja resolvida com um array de resultados na mesma ordem.

Exemplo completo: analisador de dados

Aqui está um exemplo que coloca em prática vários conceitos que vimos:

// Simular uma chamada de API
const fetchData = () => Promise.resolve([
    { id: 1, nom: "Alice", age: 25, scores: [85, 90, 78] },
    { id: 2, nom: "Bob", age: 30, scores: [92, 85, 90] },
    { id: 3, nom: "Charlie", age: 22, scores: [70, 65, 80] },
    { id: 4, nom: "David", age: 28, scores: [95, 88, 92] }
]);

// Funções puras para as operações
const calculerMoyenne = scores => 
    scores.reduce((total, score) => total + score, 0) / scores.length;

const evaluerPerformance = moyenne => {
    if (moyenne >= 90) return "Excellent";
    if (moyenne >= 80) return "Très bien";
    if (moyenne >= 70) return "Bien";
    return "Passable";
};

// Função de ordem superior para filtrar
const filtrerPar = critere => tableau => 
    tableau.filter(critere);

// Closure para manter um cache dos resultados
function createAnalyseur() {
    const cache = new Map();
    
    return {
        async analyserEtudiants(filtre = null) {
            // Verificar o cache
            const cacheKey = filtre ? filtre.toString() : "tous";
            if (cache.has(cacheKey)) {
                console.log("Utilisation du cache pour", cacheKey);
                return cache.get(cacheKey);
            }
            
            // Obter os dados
            const etudiants = await fetchData();
            
            // Aplicar o filtro, se necessário
            const donneesFiltrees = filtre 
                ? filtrerPar(filtre)(etudiants) 
                : etudiants;
            
            // Transformar os dados
            const resultat = donneesFiltrees.map(etudiant => {
                const moyenne = calculerMoyenne(etudiant.scores);
                return {
                    id: etudiant.id,
                    nom: etudiant.nom,
                    age: etudiant.age,
                    moyenne: parseFloat(moyenne.toFixed(2)),
                    performance: evaluerPerformance(moyenne)
                };
            });
            
            // Salvar no cache e retornar
            cache.set(cacheKey, resultat);
            return resultat;
        },
        
        // Limpar o cache
        clearCache() {
            cache.clear();
        }
    };
}

// Utilização
async function demoAnalyse() {
    const analyseur = createAnalyseur();
    
    // Análise de todos os estudantes
    console.log("Tous les étudiants:");
    const tous = await analyseur.analyserEtudiants();
    console.table(tous);
    
    // Filtro por desempenho
    console.log("\nÉtudiants excellents:");
    const excellents = await analyseur.analyserEtudiants(
        etudiant => calculerMoyenne(etudiant.scores) >= 90
    );
    console.table(excellents);
    
    // Filtro por idade
    console.log("\nÉtudiants de moins de 25 ans:");
    const jeunes = await analyseur.analyserEtudiants(
        etudiant => etudiant.age < 25
    );
    console.table(jeunes);
    
    // Demonstração do cache
    console.log("\nSeconde demande (depuis le cache):");
    const deuxiemeAppel = await analyseur.analyserEtudiants();
    console.table(deuxiemeAppel);
}

// Execução da demo
demoAnalyse().catch(error => console.error(error));

Conclusão

Neste tutorial, exploramos os recursos avançados das funções no JavaScript moderno:

  • As diferentes formas de definir funções, em particular as arrow functions
  • As closures e sua capacidade de encapsular dados
  • As funções de ordem superior, que manipulam outras funções
  • Os padrões funcionais, como a imutabilidade e a memoização
  • O gerenciamento de contexto com this e os métodos bind, call e apply
  • As IIFE e a criação de módulos
  • As funções geradoras e o async/await para código assíncrono

As funções estão verdadeiramente no coração do JavaScript, e dominá-las é essencial para escrever código moderno, eficiente e de fácil manutenção. Ao adotar uma abordagem funcional, você poderá criar aplicações mais robustas, testáveis e extensíveis.

No próximo tutorial, exploraremos o destructuring e o spread operator.


Precisa rever as variáveis? Consulte nosso tutorial anterior sobre variáveis.

Pronto para continuar? Passe para o próximo tutorial sobre destructuring.

Comentários

Os comentários são alimentados pelo GitHub Discussions

Conecte-se com o GitHub para participar da discussão

Link copiado!