Functions
Modern functions in JavaScript
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
thiscontext
They are not appropriate for:
- Object methods (which need to refer to the object with
this) - Constructor functions (you can’t use
newwith arrow functions) - Functions that use
arguments(arrow functions don’t have anargumentsobject)
// 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
- Isolating variables: Avoid polluting the global scope
- Encapsulating data: Create simple modules
- Running code immediately: Initialize applications
- 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:
-
Advanced filtering: Write a higher-order function
filtrerParthat takes a criterion (a function) and returns a function that filters an array based on that criterion. -
Debounce: Create a
debouncefunction that limits how often a function can run. This is useful for event handlers such as real-time search. -
Pipeline: Implement a
pipelinefunction that takes several functions and runs them in sequence, passing the result of each function to the next. -
Custom memoization: Improve the
memoizefunction so that it supports a custom key-generation function. -
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
thisand thebind,call, andapplymethods - 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