0%
Modern JavaScript: Functions

Functions

Modern functions in JavaScript

10-15 min

Modern JavaScript: Advanced Functions

Functions are at the heart of JavaScript and are one of its most powerful features. In this tutorial, we’ll explore the modern features of JavaScript functions, including arrow functions, closures, higher-order functions, and essential functional patterns.

JavaScript is often described as a language with “first-class functions,” which means that functions are treated like any other variable. This characteristic provides great flexibility and enables powerful programming patterns.

A Refresher on Function Basics

Before exploring advanced concepts, let’s review the different ways to define functions in JavaScript.

Function Declaration

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

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

Function Expression

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

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

Arrow Function (ES6)

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

// Concise version for simple functions
const saluerConcis = nom => `Bonjour, ${nom} !`;

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

Arrow Functions

Arrow functions, introduced with ES6, offer a concise syntax and specific behaviors that make them particularly useful in certain contexts.

Simplified Syntax

// Standard function
const double = function(x) {
    return x * 2;
};

// Equivalent arrow function
const doubleArrow = (x) => {
    return x * 2;
};

// Ultra-concise version (no braces or explicit return)
const doubleConc = x => x * 2;

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

Parentheses and Parameters

// No parameters - parentheses required
const salut = () => "Bonjour !";

// A single parameter - parentheses optional
const carre = x => x * x;

// Multiple parameters - parentheses required
const somme = (a, b) => a + b;

// Returning an object - parentheses required around the object
const creerPersonne = (nom, age) => ({ nom, age });
console.log(creerPersonne("Alice", 30)); // { nom: "Alice", age: 30 }

Behavior of this

The main functional difference between arrow functions and regular functions is how they handle the this keyword.

// In a regular function, this depends on the call context
function FonctionClassique() {
    this.valeur = 42;
    setTimeout(function() {
        console.log(this.valeur); // undefined (this refers to the global object)
    }, 1000);
}

// In an arrow function, this is lexical (inherited from the parent context)
function FonctionAvecFleche() {
    this.valeur = 42;
    setTimeout(() => {
        console.log(this.valeur); // 42 (this is the one from FonctionAvecFleche)
    }, 1000);
}

When to Use Arrow Functions

Arrow functions are ideal for:

  • Short, simple functions
  • Callbacks, particularly in array methods (map, filter, etc.)
  • Anonymous functions
  • Cases where you want to preserve the this context

They are not appropriate for:

  • Object methods (which need to refer to the object with this)
  • Constructor functions (you can’t use new with arrow functions)
  • Functions that use arguments (arrow functions don’t have an arguments object)
// Example of use with array methods
const nombres = [1, 2, 3, 4, 5];
const carres = nombres.map(x => x * x); // [1, 4, 9, 16, 25]

// Don't use for object methods
const objet = {
    valeur: 42,
    // Bad approach
    mauvaiseFonction: () => {
        console.log(this.valeur); // undefined, this is the global object
    },
    // Good approach
    bonneFonction() {
        console.log(this.valeur); // 42
    }
};

Advanced Function Parameters

ES6 and later versions introduced several features for handling parameters in a more flexible and expressive way.

Default Parameters

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 !"

// Expressions as default values
function getDateFormatee(date = new Date()) {
    return date.toLocaleDateString();
}

// Using earlier parameters as default values
function calculer(a = 1, b = a * 2) {
    return a + b;
}
console.log(calculer()); // 3 (1 + 2)

Rest Parameters

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

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

// Combining with regular parameters
function formatMessage(prefix, suffix, ...messages) {
    return messages.map(msg => `${prefix}${msg}${suffix}`);
}

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

Parameter Destructuring

// Object destructuring in parameters
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"

// Array destructuring
function afficherCoordonnees([x, y, z = 0]) {
    console.log(`X: ${x}, Y: ${y}, Z: ${z}`);
}

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

Closures

Closures are one of the most powerful concepts in JavaScript, allowing a function to “remember” its lexical environment even after its parent function has finished executing.

Understanding Closures

function creerCompteur() {
    let compte = 0; // Private variable
    
    return function() {
        compte++; // Access to the parent scope's variable
        return compte;
    };
}

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

// The compte variable is "encapsulated" in the closure
// and inaccessible from the outside

Practical Applications of Closures

Data Encapsulation

function creerBanque(soldeInitial) {
    let solde = soldeInitial; // Private variable
    
    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 is not directly accessible: console.log(monCompte.solde) // undefined

Creating Custom Functions

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

Managing Asynchronous Context

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');
// Later in the code
getUserData(data => {
    console.log("Données reçues:", data);
});

The Classic Loop and Closure Problem

A common problem with closures occurs when using loop variables in asynchronous functions:

// Problem
function probleme() {
    const fonctions = [];
    
    for (var i = 0; i < 3; i++) {
        fonctions.push(function() {
            console.log(i);
        });
    }
    
    // At this point, i has become 3
    return fonctions;
}

const fns = probleme();
fns[0](); // 3 (not 0 as you might expect)
fns[1](); // 3
fns[2](); // 3

// Solution with let (block scope)
function solution() {
    const fonctions = [];
    
    for (let i = 0; i < 3; i++) {
        // Each iteration creates a new i
        fonctions.push(function() {
            console.log(i);
        });
    }
    
    return fonctions;
}

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

// Solution with an explicit closure (for 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;
}

Higher-Order Functions

Higher-order functions are functions that take other functions as arguments or return functions. They are fundamental to functional programming in JavaScript.

Functions That Accept Callbacks

// Higher-order function that takes a function as a parameter
function appliquerOperation(nombre, operation) {
    return operation(nombre);
}

// Functions that can be used as 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

Functions That Return Functions

// Currying - transforming a function with multiple arguments
// into a sequence of single-argument functions
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

// Function composition
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

Examples of Built-In Higher-Order Functions

JavaScript has several built-in higher-order methods for arrays:

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

// map - transforms each element
const doubles = nombres.map(n => n * 2); // [2, 4, 6, 8, 10, 12]

// filter - selects certain elements
const pairs = nombres.filter(n => n % 2 === 0); // [2, 4, 6]

// reduce - accumulates values
const somme = nombres.reduce((acc, n) => acc + n, 0); // 21

// find - finds the first element that satisfies a condition
const premierPair = nombres.find(n => n % 2 === 0); // 2

// every - checks whether all elements satisfy a condition
const tousSontPositifs = nombres.every(n => n > 0); // true

// some - checks whether at least one element satisfies a condition
const auMoinsUnGrand = nombres.some(n => n > 4); // true

Advanced Functional Patterns

JavaScript makes it possible to implement many patterns from functional programming.

Pure Functions

A pure function has no side effects and always returns the same result for the same inputs:

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

// Impure function (side effect)
let total = 0;
function ajouterAuTotal(valeur) {
    total += valeur; // Modifies external state
    return total;
}

Immutability

Immutability means never modifying data, but instead creating new versions of it:

// Immutable approach
const array1 = [1, 2, 3];
const array2 = [...array1, 4]; // Creates a new array
// array1 is still [1, 2, 3]

const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // New object
// obj1 is still { a: 1, b: 2 }

Memoization

Memoization is an optimization technique that consists of caching the results of function calls:

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;
    };
}

// Expensive recursive function
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Optimized version with 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)); // Very slow
console.timeEnd("sans memo");

console.time("avec memo");
console.log(fiboMemo(35)); // Much faster
console.timeEnd("avec memo");

Execution Context and this

Managing the this context is crucial in JavaScript and is a source of much confusion.

Controlling the this Context

JavaScript provides several methods for controlling the execution context:

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

// Normal approach
console.log(personne.saluer()); // "Bonjour, je suis Alice"

// Loss of context
const fonctionSaluer = personne.saluer;
console.log(fonctionSaluer()); // "Bonjour, je suis undefined"

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

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

// Solution 3: apply (like call but takes an array of arguments)
console.log(personne.saluer.apply({ nom: "Charlie" })); // "Bonjour, je suis Charlie"

Differences Between call, apply, and bind

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

const user = { nom: "Alice" };

// call: invokes immediately with separate arguments
console.log(presenter.call(user, "Bonjour", "!")); // "Bonjour Alice !"

// apply: invokes immediately with arguments in an array
console.log(presenter.apply(user, ["Salut", "..."])); // "Salut Alice ..."

// bind: creates a new function bound to the context, without invoking it
const presenterUtilisateur = presenter.bind(user, "Bienvenue");
console.log(presenterUtilisateur("!")); // "Bienvenue Alice !"

Self-Invoking Functions and IIFEs

IIFE (Immediately Invoked Function Expression)

IIFEs are functions that run immediately after they are defined:

// Classic syntax
(function() {
    const message = "Je m'exécute immédiatement";
    console.log(message);
})();

// With parameters
(function(nom) {
    console.log(`Bonjour, ${nom} !`);
})("Alice");

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

Uses of IIFEs

  1. Isolating variables: Avoid polluting the global scope
  2. Encapsulating data: Create simple modules
  3. Running code immediately: Initialize applications
  4. Creating closures: Capture values from the surrounding environment
// Module example with an IIFE
const compteur = (function() {
    let compte = 0; // Private variable
    
    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

Generator Functions and async/await

Generator functions and async/await are powerful mechanisms for managing complex execution flows.

Generator Functions

Generator functions can pause and resume their execution:

function* compterJusqua(n) {
    for (let i = 1; i <= n; i++) {
        yield i; // pauses execution and returns the value
    }
}

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

// Iteration with for...of
for (const valeur of compterJusqua(3)) {
    console.log(valeur); // 1, 2, 3
}

async and await Functions

async/await functions simplify the handling of asynchronous code:

// Function that returns a Promise
function attendre(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// Asynchronous function using 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é";
}

// Usage
processAsync().then(resultat => {
    console.log(resultat); // "Terminé" after 2 seconds
});

// Error handling
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; // rethrow for later handling
    }
}

Practice Exercises

To solidify your knowledge, try these exercises:

  1. Advanced filtering: Write a higher-order function filtrerPar that takes a criterion (a function) and returns a function that filters an array based on that criterion.

  2. Debounce: Create a debounce function that limits how often a function can run. This is useful for event handlers such as real-time search.

  3. Pipeline: Implement a pipeline function that takes several functions and runs them in sequence, passing the result of each function to the next.

  4. Custom memoization: Improve the memoize function so that it supports a custom key-generation function.

  5. Promise manager: Create a function that takes an array of promises and returns a new promise that resolves with an array of results in the same order.

Complete Example: Data Analyzer

Here is an example that puts several of the concepts we have covered into practice:

// Simulate an API call
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] }
]);

// Pure functions for the operations
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";
};

// Higher-order function for filtering
const filtrerPar = critere => tableau => 
    tableau.filter(critere);

// Closure to keep a cache of results
function createAnalyseur() {
    const cache = new Map();
    
    return {
        async analyserEtudiants(filtre = null) {
            // Check the cache
            const cacheKey = filtre ? filtre.toString() : "tous";
            if (cache.has(cacheKey)) {
                console.log("Utilisation du cache pour", cacheKey);
                return cache.get(cacheKey);
            }
            
            // Fetch the data
            const etudiants = await fetchData();
            
            // Apply the filter if needed
            const donneesFiltrees = filtre 
                ? filtrerPar(filtre)(etudiants) 
                : etudiants;
            
            // Transform the data
            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)
                };
            });
            
            // Save to the cache and return
            cache.set(cacheKey, resultat);
            return resultat;
        },
        
        // Clear the cache
        clearCache() {
            cache.clear();
        }
    };
}

// Usage
async function demoAnalyse() {
    const analyseur = createAnalyseur();
    
    // Analysis of all students
    console.log("Tous les étudiants:");
    const tous = await analyseur.analyserEtudiants();
    console.table(tous);
    
    // Filter by performance
    console.log("\nÉtudiants excellents:");
    const excellents = await analyseur.analyserEtudiants(
        etudiant => calculerMoyenne(etudiant.scores) >= 90
    );
    console.table(excellents);
    
    // Filter by age
    console.log("\nÉtudiants de moins de 25 ans:");
    const jeunes = await analyseur.analyserEtudiants(
        etudiant => etudiant.age < 25
    );
    console.table(jeunes);
    
    // Cache demonstration
    console.log("\nSeconde demande (depuis le cache):");
    const deuxiemeAppel = await analyseur.analyserEtudiants();
    console.table(deuxiemeAppel);
}

// Run the demo
demoAnalyse().catch(error => console.error(error));

Conclusion

In this tutorial, we explored the advanced features of functions in modern JavaScript:

  • The different ways to define functions, especially arrow functions
  • Closures and their ability to encapsulate data
  • Higher-order functions that manipulate other functions
  • Functional patterns such as immutability and memoization
  • Context management with this and the bind, call, and apply methods
  • IIFEs and module creation
  • Generator functions and async/await for asynchronous code

Functions are truly at the heart of JavaScript, and mastering them is essential for writing modern, efficient, and maintainable code. By adopting a functional approach, you can build applications that are more robust, testable, and extensible.

In the next tutorial, we will explore destructuring and the spread operator.


Need to review variables? Check out our previous tutorial on variables.

Ready to continue? Move on to the next tutorial on destructuring.

Comments

Comments are powered by GitHub Discussions

Sign in with GitHub to join the discussion

Link copied!