Jour 43 Day 43 · mercredi 7 octobre 2026 Wednesday 7 October 2026 Web Intermédiaire

Closures, prototypes & this Closures, prototypes & this

Les trois mécanismes qui font trébucher en entretien front : la closure (LA question JS la plus posée), la chaîne de prototypes derrière le mot-clé class, et les quatre règles de this. The three mechanisms that trip candidates up in front-end interviews: the closure (THE most asked JS question), the prototype chain behind the class keyword, and the four rules of this.

L’essentiel

Trois mécanismes du cœur de JavaScript concentrent l’essentiel des questions d’entretien front — parce qu’ils révèlent si vous comprenez le langage ou si vous le récitez.

Une closure est une fonction qui capture les variables de son scope lexical de définition et les garde vivantes après la fin de la fonction englobante. « Lexical » signifie : déterminé par la position du code dans le fichier, pas par la façon dont la fonction sera appelée. Chaque fonction JS emporte avec elle l’environnement où elle est née.

Les prototypes sont le vrai mécanisme d’héritage de JavaScript : chaque objet possède un lien interne [[Prototype]] vers un autre objet. Lire une propriété absente déclenche une remontée de cette chaîne de prototypes jusqu’à null. Le mot-clé class (ES2015) n’a rien changé au moteur : c’est du sucre syntaxique au-dessus des prototypes.

this n’est pas « l’objet courant » : sa valeur dépend du site d’appel, pas du lieu de définition — quatre règles suffisent à couvrir tous les cas, plus l’exception des arrow functions qui n’ont pas de this propre.

En survol, deux compagnons de route : le hoisting (les déclarations var et function sont remontées en tête de scope) et la TDZ (temporal dead zone : let/const sont hissées aussi, mais y accéder avant la ligne de déclaration jette une ReferenceError).

Comment ça marche

La closure, par le piège classique — la question de code la plus posée en entretien JS :

// Le piège : var n'a pas de scope de bloc
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i));   // → 3, 3, 3
}
// Les 3 callbacks capturent LA MÊME variable i
// (une closure référence la variable, pas sa valeur).
// Quand ils s'exécutent, la boucle est finie : i === 3.

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j));   // → 0, 1, 2
}
// let crée une NOUVELLE liaison j à chaque itération :
// chaque callback capture la sienne.

// Usage réel : de l'état privé (pattern module)
function makeCounter() {
  let count = 0;               // invisible de l'extérieur
  return () => ++count;        // la closure garde count vivant
}
const next = makeCounter();
next(); // 1
next(); // 2 — l'état survit entre les appels

Les usages réels sont partout : compteurs et état privé, modules (exposer une API, cacher l’implémentation), callbacks qui se souviennent de leur contexte, memoization, debounce/throttle. Les hooks React reposent entièrement dessus — le fameux bug de la « stale closure » dans useEffect, c’est exactement le piège du var ci-dessus.

La chaîne de prototypes — lire rex.eat() déclenche une recherche ascendante :

rex { name: "Rex" }
  │ [[Prototype]]
  ▼
Dog.prototype { bark() }
  │ [[Prototype]]        ← posé par extends
  ▼
Animal.prototype { eat() }   ← trouvé ici !
  │ [[Prototype]]
  ▼
Object.prototype { toString(), hasOwnProperty()… }
  │
  ▼
null                     ← fin de la chaîne

class Dog extends Animal ne fait que câbler ce schéma : les méthodes vont sur Dog.prototype, et Dog.prototype.[[Prototype]] pointe vers Animal.prototype. Un détail qui fait mouche en entretien : les méthodes ne sont pas copiées dans chaque instance — mille chiens partagent l’unique fonction bark de Dog.prototype.

Les quatre règles de this — à évaluer au site d’appel, par ordre de priorité décroissante :

RègleForme d’appelthis vaut
1. newnew User()L’objet fraîchement créé
2. Explicitef.call(ctx) / f.apply(ctx) / f.bind(ctx)Le ctx passé en argument
3. Méthodeobj.f()L’objet avant le point
4. Appel simplef()undefined en strict mode (globalThis sinon)

Les arrow functions court-circuitent tout : elles n’ont pas de this propre et utilisent celui du scope lexical englobant — le this de l’endroit où elles sont écrites. Idéal pour les callbacks (setTimeout(() => this.tick()) dans une classe), catastrophique comme méthode d’objet (this ne sera jamais l’objet).

🎤 En entretien — « c’est quoi une closure ? » est LA question JS la plus posée. Réponse en trois temps : la définition (« une fonction qui capture les variables de son scope de définition et les garde vivantes après la fin de la fonction englobante »), un exemple concret (le compteur makeCounter), un usage réel (« c’est ce qui permet l’état privé — et tout le modèle des hooks React »). Définition + exemple + usage : imparable.

Concepts clés à maîtriser

  • Une closure capture des variables, pas des valeurs — c’est une référence vivante : si la variable change après la création de la closure, la closure voit la nouvelle valeur. Tout le piège du var dans la boucle tient dans cette phrase.
  • prototype vs [[Prototype]] — deux choses différentes : prototype est une propriété des fonctions constructrices (le futur [[Prototype]] de leurs instances) ; [[Prototype]] est le lien interne de chaque objet, lisible via Object.getPrototypeOf(obj) (le vieux __proto__ est déprécié).
  • class est du sucre — sous le capot : des fonctions constructrices et des prototypes câblés. Mais un sucre utile : syntaxe claire, appel sans new interdit, super propre. Le dire ainsi montre les deux niveaux de lecture.
  • La méthode détachée perd son this — const f = obj.method; f() : règle 4, appel simple, this === undefined. Le grand classique des handlers d’événements. Trois remèdes : obj.method.bind(obj), une arrow () => obj.method(), ou un champ de classe method = () => {…}.
  • Hoisting & TDZ en deux phrases — var est hissée et initialisée à undefined (lisible trop tôt, sans erreur : source de bugs silencieux) ; let/const sont hissées mais non initialisées : y toucher avant la déclaration jette une ReferenceError. La TDZ transforme un bug silencieux en erreur franche — c’est un progrès.

💡 Le réflexe qui sauve — pour trouver this, ne regardez jamais où la fonction est définie (sauf arrow function) : regardez comment elle est appelée. Y a-t-il un new ? Un .call/.bind ? Un objet avant le point ? Rien ? Les quatre règles, dans cet ordre.

En entretien

« C’est quoi une closure ? Donne un usage réel. » — Définition exacte (fonction + variables capturées de son scope de définition, qui survivent), exemple du compteur, usages : état privé, modules, callbacks, debounce, hooks React. Voir le callout ci-dessus pour la structure.

« Que va afficher cette boucle for (var i…) avec setTimeout ? » — « 3, 3, 3 » : les trois callbacks partagent la même variable i, lue après la fin de la boucle. Correctif : let (nouvelle liaison par itération) ou une IIFE qui fige la valeur. Expliquer le pourquoi (capture de variable, pas de valeur) fait la différence.

« Comment marche l’héritage en JavaScript ? » — Par délégation le long de la chaîne de prototypes : une propriété absente est cherchée sur [[Prototype]], puis remontée jusqu’à null. class/extends ne sont que du sucre au-dessus. Bonus : les méthodes sont partagées via le prototype, pas copiées par instance.

« Les quatre règles de this ? » — new > call/apply/bind > méthode (obj.f()) > appel simple (undefined en strict). Et l’exception : les arrow functions n’ont pas de this propre, elles prennent celui du scope où elles sont écrites.

« Pourquoi const f = obj.method; f() casse, et comment corriger ? » — Détacher la méthode fait retomber sur la règle de l’appel simple : this vaut undefined. Corrections : bind, wrapper arrow, ou champ de classe en arrow. Citer le cas réel : addEventListener(this.handleClick) en React classe.

Pièges & idées reçues

⚠️ L’arrow function n’est pas « la nouvelle syntaxe de fonction » — c’est une fonction sans this, sans arguments, non constructible (new interdit). En faire une méthode d’objet (obj = { greet: () => this.name }) est le piège inverse du candidat qui a retenu « arrow = bien » : ici this ne sera jamais obj.

  • « Une closure copie les valeurs » — non : elle référence les variables. C’est précisément pour ça que la boucle var affiche 3, 3, 3 et non 0, 1, 2.
  • « class a apporté de vraies classes comme en Java » — non : le modèle reste prototypal, class est une syntaxe. Un typeof Dog renvoie "function".
  • prototype confondu avec __proto__ — Dog.prototype (propriété de la fonction) deviendra le [[Prototype]] des instances ; rex.__proto__ (accès déprécié au lien interne) est Dog.prototype. Les confondre trahit une compréhension récitée.
  • Les closures et la mémoire — une closure garde vivant tout ce qu’elle capture : un listener qui capture un gros objet et n’est jamais retiré (removeEventListener oublié) est une fuite mémoire classique en SPA.
  • Hoisting mal compris — « var remonte la déclaration et l’assignation » : faux, seule la déclaration remonte ; la variable vaut undefined jusqu’à la ligne d’assignation.

Pour aller plus loin

The essentials

Three mechanisms at the core of JavaScript concentrate most front-end interview questions — because they reveal whether you understand the language or merely recite it.

A closure is a function that captures the variables of its lexical definition scope and keeps them alive after the enclosing function has returned. “Lexical” means: determined by where the code sits in the file, not by how the function will be called. Every JS function carries along the environment it was born in.

Prototypes are JavaScript’s real inheritance mechanism: every object holds an internal [[Prototype]] link to another object. Reading a missing property triggers a walk up this prototype chain until null. The class keyword (ES2015) changed nothing in the engine: it is syntactic sugar over prototypes.

this is not “the current object”: its value depends on the call site, not on where the function was defined — four rules cover every case, plus the arrow-function exception, which has no this of its own.

Two travel companions, in passing: hoisting (var and function declarations are moved to the top of their scope) and the TDZ (temporal dead zone: let/const are hoisted too, but touching them before their declaration line throws a ReferenceError).

How it works

The closure, via the classic trap — the most asked coding question in JS interviews:

// The trap: var has no block scope
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i));   // → 3, 3, 3
}
// All 3 callbacks capture THE SAME variable i
// (a closure references the variable, not its value).
// By the time they run, the loop is over: i === 3.

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j));   // → 0, 1, 2
}
// let creates a NEW binding j on each iteration:
// each callback captures its own.

// Real use: private state (module pattern)
function makeCounter() {
  let count = 0;               // invisible from outside
  return () => ++count;        // the closure keeps count alive
}
const next = makeCounter();
next(); // 1
next(); // 2 — the state survives between calls

Real uses are everywhere: counters and private state, modules (expose an API, hide the implementation), callbacks that remember their context, memoization, debounce/throttle. React hooks rest entirely on closures — the famous “stale closure” bug in useEffect is exactly the var trap above.

The prototype chain — reading rex.eat() triggers an upward lookup:

rex { name: "Rex" }
  │ [[Prototype]]
  ▼
Dog.prototype { bark() }
  │ [[Prototype]]        ← wired by extends
  ▼
Animal.prototype { eat() }   ← found here!
  │ [[Prototype]]
  ▼
Object.prototype { toString(), hasOwnProperty()… }
  │
  ▼
null                     ← end of the chain

class Dog extends Animal merely wires this diagram: methods go on Dog.prototype, and Dog.prototype.[[Prototype]] points to Animal.prototype. A detail that lands well in interviews: methods are not copied into each instance — a thousand dogs share the single bark function on Dog.prototype.

The four rules of this — evaluated at the call site, in decreasing order of priority:

RuleCall formthis is
1. newnew User()The freshly created object
2. Explicitf.call(ctx) / f.apply(ctx) / f.bind(ctx)The ctx passed as argument
3. Methodobj.f()The object before the dot
4. Plain callf()undefined in strict mode (globalThis otherwise)

Arrow functions short-circuit all of it: they have no this of their own and use the one from the enclosing lexical scope — the this of the place where they are written. Ideal for callbacks (setTimeout(() => this.tick()) inside a class), disastrous as an object method (this will never be the object).

🎤 In an interview — “what is a closure?” is THE most asked JS question. Answer in three beats: the definition (“a function that captures the variables of its definition scope and keeps them alive after the enclosing function returns”), a concrete example (the makeCounter counter), a real use (“that’s what enables private state — and the whole React hooks model”). Definition + example + use: unbeatable.

Key concepts to master

  • A closure captures variables, not values — it’s a live reference: if the variable changes after the closure is created, the closure sees the new value. The entire var-in-a-loop trap fits in that sentence.
  • prototype vs [[Prototype]] — two different things: prototype is a property of constructor functions (the future [[Prototype]] of their instances); [[Prototype]] is every object’s internal link, readable via Object.getPrototypeOf(obj) (the old __proto__ is deprecated).
  • class is sugar — under the hood: constructor functions and wired prototypes. But useful sugar: clear syntax, calling without new forbidden, clean super. Saying it this way shows both levels of understanding.
  • A detached method loses its this — const f = obj.method; f(): rule 4, plain call, this === undefined. The great classic of event handlers. Three remedies: obj.method.bind(obj), an arrow () => obj.method(), or a class field method = () => {…}.
  • Hoisting & TDZ in two sentences — var is hoisted and initialized to undefined (readable too early, without error: a source of silent bugs); let/const are hoisted but uninitialized: touching them before the declaration throws a ReferenceError. The TDZ turns a silent bug into a loud error — that’s progress.

💡 The reflex that saves you — to find this, never look at where the function is defined (except for arrow functions): look at how it is called. Is there a new? A .call/.bind? An object before the dot? Nothing? The four rules, in that order.

In an interview

“What is a closure? Give a real use.” — Exact definition (function + captured variables from its definition scope, which survive), the counter example, uses: private state, modules, callbacks, debounce, React hooks. See the callout above for the structure.

“What will this for (var i…) loop with setTimeout print?” — “3, 3, 3”: the three callbacks share the same variable i, read after the loop has ended. Fix: let (a new binding per iteration) or an IIFE that freezes the value. Explaining the why (capture of the variable, not the value) makes the difference.

“How does inheritance work in JavaScript?” — By delegation along the prototype chain: a missing property is looked up on [[Prototype]], walking up until null. class/extends are just sugar on top. Bonus: methods are shared via the prototype, not copied per instance.

“The four rules of this?” — new > call/apply/bind > method (obj.f()) > plain call (undefined in strict mode). And the exception: arrow functions have no this of their own, they take the one from the scope where they are written.

“Why does const f = obj.method; f() break, and how do you fix it?” — Detaching the method falls back to the plain-call rule: this is undefined. Fixes: bind, an arrow wrapper, or an arrow class field. Cite the real case: addEventListener(this.handleClick) in class-based React.

Pitfalls & misconceptions

⚠️ The arrow function is not “the new function syntax” — it’s a function without this, without arguments, not constructible (new forbidden). Making it an object method (obj = { greet: () => this.name }) is the reverse trap for the candidate who memorized “arrow = good”: here this will never be obj.

  • “A closure copies values” — no: it references variables. That’s precisely why the var loop prints 3, 3, 3 and not 0, 1, 2.
  • “class brought real classes like Java” — no: the model stays prototypal, class is syntax. typeof Dog returns "function".
  • prototype confused with __proto__ — Dog.prototype (a property of the function) will become the instances’ [[Prototype]]; rex.__proto__ (deprecated access to the internal link) is Dog.prototype. Mixing them up betrays recited understanding.
  • Closures and memory — a closure keeps alive everything it captures: a listener capturing a big object and never removed (removeEventListener forgotten) is a classic SPA memory leak.
  • Hoisting misunderstood — “var hoists the declaration and the assignment”: false, only the declaration moves up; the variable is undefined until the assignment line.

Going further

S'entraîner sur ce sujet → Practice this topic →