12k
All articles

10 Questions d'entretien JavaScript et ce qu'elles évaluent vraiment

10 questions d entretien JavaScript expliquées par le hoisting, les closures, this, la boucle dévénement et la coercition, avec sorties et tests.

OpenReplay Team
OpenReplay Team
10 Questions d'entretien JavaScript et ce qu'elles évaluent vraiment

La plupart des questions d’entretien JavaScript ne cherchent pas à savoir si vous connaissez le résultat — elles cherchent à savoir si vous comprenez le modèle qui le produit. Un interviewer senior qui écrit console.log(x); var x = 5; au tableau blanc sait déjà que cela affiche undefined. Ce qu’il évalue, c’est votre capacité à expliquer pourquoi sans recourir à la formule « le hoisting déplace la déclaration en haut », car cette formule décrit le modèle mental, pas le mécanisme. Les candidats qui progressent sont ceux qui savent nommer le comportement à l’exécution, prédire le résultat et articuler leur raisonnement à voix haute en deux phrases claires.

Cet article présente dix questions couvrant chacune un fondamental distinct — le modèle d’exécution, la Temporal Dead Zone, les closures, la portée lexicale, le binding de this, l’event loop et la coercition — et décompose chacune en quatre parties : le snippet, le résultat exact et son explication, ce que l’interviewer évalue vraiment, et la question de suivi qui suivra. Chaque résultat est défini par la spécification et reproductible dans tout moteur actuel. Ces comportements sont stables dans tous les moteurs actuels — ce sont des sémantiques du langage, pas des particularités liées à une version.

Points clés

  • La question sur le hoisting de var ne teste pas si vous savez que le résultat est undefined — elle teste si vous comprenez que JavaScript crée les liaisons de variables à l’entrée d’une portée, avant l’exécution de toute ligne.
  • let et const sont également hissés, mais ils restent non initialisés dans la Temporal Dead Zone jusqu’à leur ligne de déclaration, de sorte que les lire trop tôt lève une ReferenceError au lieu de retourner undefined.
  • Dans le bug classique du setTimeout dans une boucle for, var affiche la valeur finale de la boucle (3 3 3) parce que tous les callbacks partagent une seule liaison à portée de fonction, tandis que let affiche la valeur de chaque itération (0 1 2) parce qu’il crée une nouvelle liaison par itération.
  • this n’est pas déterminé là où une fonction est écrite — il est déterminé par la façon dont la fonction est appelée ; les fonctions fléchées sont l’exception car elles n’ont pas de this propre.
  • Les callbacks de Promise (microtâches) s’exécutent toujours avant les callbacks de setTimeout (macrotâches), même avec un délai de 0ms.

Questions d’entretien JavaScript sur le hoisting et le modèle d’exécution

1. Pourquoi var affiche-t-il undefined avant l’assignation ?

console.log(x); // undefined
var x = 20;
console.log(x); // 20

La première ligne affiche undefined, pas une ReferenceError. L’explication courante est que la déclaration est « déplacée en haut », mais c’est une métaphore. Ce qui se passe réellement : lorsque le moteur entre dans une portée, il instancie les liaisons de cette portée avant d’exécuter toute instruction, et une liaison var est initialisée à undefined à ce moment. L’assignation = 20 reste sur sa ligne d’origine et s’exécute dans l’ordre. Cette étape de création des liaisons est définie dans la spécification du langage ECMAScript sous l’instanciation de l’environnement de variables — rien n’est physiquement déplacé.

Ce qu’ils évaluent vraiment : si vous comprenez que JS dispose d’une phase de création avant la phase d’exécution. La réponse undefined est anecdotique ; le modèle d’exécution en deux phases est la compétence visée. Consultez l’entrée Hoisting du glossaire MDN pour le cadrage canonique.

Question de suivi probable : « Et si c’était let à la place de var ? » — ce qui correspond à la question 2.

2. Pourquoi let lève-t-il une erreur au lieu d’afficher undefined ?

console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 20;

let et const sont hissés — la liaison est créée à l’entrée de la portée — mais ils restent non initialisés jusqu’à ce que l’exécution atteigne la ligne de déclaration. La fenêtre entre l’entrée dans la portée et la ligne de déclaration est la Temporal Dead Zone, et lire une liaison à l’intérieur de celle-ci lève une ReferenceError: Cannot access 'y' before initialization. C’est la distinction fondamentale : var est initialisé à undefined à la création ; let/const ne sont pas du tout initialisés jusqu’à l’exécution de leur ligne. MDN documente cela dans la section Temporal Dead Zone de let.

Ce qu’ils évaluent vraiment : si vous distinguez la création d’une liaison de son initialisation. Un candidat qui dit « let n’est pas hissé » a le mauvais modèle ; la bonne réponse est qu’il est hissé mais non initialisé.

Question de suivi probable : « Alors pourquoi la TDZ existe-t-elle ? » — dites qu’elle rend const applicable et transforme une utilisation avant déclaration en une erreur explicite plutôt qu’un undefined silencieux.

Voici le tableau de référence que les interviewers s’attendent à vous voir reconstituer verbalement :

Comportementvarletconst
Hissé (liaison créée à l’entrée de la portée)OuiOuiOui
Initialisé à la créationundefinedNon (TDZ)Non (TDZ)
Lecture avant déclarationundefinedReferenceErrorReferenceError
PortéeFonctionBlocBloc
Redéclarable dans la même portéeOuiNonNon
RéassignableOuiOuiNon

3. Hoisting des déclarations de fonctions vs. des expressions de fonctions

foo(); // "I run"
bar(); // TypeError: bar is not a function

function foo() { console.log("I run"); }
var bar = function () { console.log("I don't"); };

Une déclaration de fonction est hissée entièrement — le nom et le corps — donc foo() fonctionne avant sa définition. Une expression de fonction assignée à var bar ne hisse que la liaison bar, initialisée à undefined. Appeler undefined lève une TypeError, pas une ReferenceError — la liaison existe, elle n’est simplement pas encore une fonction. Identifier correctement le type d’erreur est le signe révélateur ici. MDN couvre cette distinction dans la documentation sur les déclarations de fonctions.

Ce qu’ils évaluent vraiment : si vous comprenez que les déclarations hissent des valeurs tandis que les expressions ne hissent que des liaisons. C’est la même distinction que dans les questions 1 et 2, appliquée aux fonctions.

Question de suivi probable : « Et si bar était déclaré avec let ? » — alors l’appel anticipé lève une ReferenceError depuis la TDZ au lieu d’une TypeError.

Questions d’entretien JavaScript sur les closures et la portée

4. Le classique setTimeout dans une boucle

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3 3 3
}

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 0 1 2
}

La boucle avec var affiche 3 3 3. Il n’existe qu’un seul i à portée de fonction ; au moment où les callbacks s’exécutent (après la fin de la boucle synchrone), i vaut 3, et les trois closures lisent cette même liaison. La boucle avec let affiche 0 1 2 parce que let crée une nouvelle liaison pour chaque itération, de sorte que chaque callback ferme sur son propre i. C’est le snippet le plus mal compris du web, et il empile trois concepts : les closures, la portée et l’event loop (les callbacks sont différés, donc la boucle se termine en premier). Le guide sur les Closures de MDN documente la liaison let par itération.

Ce qu’ils évaluent vraiment : si vous pouvez raisonner sur quand une closure lit une variable par rapport à quand elle a été créée. Ce n’est pas anecdotique — c’est la même classe de bug qui part en production sous forme d’état de closure périmée dans les gestionnaires d’événements et les effets React. Les replays de session de gestionnaires à closure périmée les montrent fréquemment en train d’enregistrer une valeur capturée lors d’un rendu antérieur.

Question de suivi probable : « Corrigez-le sans let. » — enveloppez le corps dans une IIFE qui prend i comme argument, créant une nouvelle portée par itération.

5. Le compteur privé

function makeCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    value: () => count,
  };
}

const c = makeCounter();
c.increment(); // 1
c.increment(); // 2
c.value();     // 2
// count est inaccessible depuis l'extérieur

Une closure est une fonction associée aux variables auxquelles elle avait accès là où elle a été définie, et non là où elle est appelée — c’est pourquoi les méthodes retournées accèdent toujours à count après que makeCounter a retourné. Comme rien à l’extérieur n’expose count directement, il est effectivement privé. Il s’agit d’une encapsulation construite à partir de la portée, et non d’un champ #private.

Ce qu’ils évaluent vraiment : si vous comprenez les closures comme un outil de gestion mémoire et d’encapsulation, et pas seulement comme une réponse à un quiz. La question de suivi sonde si vous connaissez le coût associé.

Question de suivi probable : « Cela provoque-t-il une fuite mémoire ? » — la variable count reste en vie tant que c est accessible, parce que la closure en détient une référence ; c’est intentionnel ici, mais des closures non contrôlées sur des objets volumineux sont une vraie source de fuites.

6. La portée lexicale : définie, pas appelée

const x = 10;

function outer() {
  const x = 20;
  return inner;
}

function inner() {
  console.log(x); // 10
}

outer()(); // 10

inner affiche 10, pas 20. JavaScript résout les variables libres en fonction de l’endroit où une fonction est définie dans le code source, et non de l’endroit où elle est appelée. inner est définie au niveau supérieur, donc son x se résout au 10 du niveau supérieur — le x à l’intérieur de outer lui est sans rapport. C’est la portée lexicale (statique), et c’est ce qui rend les closures prévisibles.

Ce qu’ils évaluent vraiment : si vous confondez la pile d’appels avec la chaîne de portées. Les langages à portée dynamique afficheraient 20 ; JavaScript ne le fait pas.

Question de suivi probable : « Déplacez maintenant la déclaration de inner à l’intérieur de outer. » — elle se résout alors à 20, parce que son site de définition a changé.

Questions d’entretien JavaScript sur le binding de this

7. À quoi this fait-il référence ici ?

const user = {
  name: "Ada",
  greet() { return this.name; },
};

const fn = user.greet;
user.greet(); // "Ada"
fn();         // undefined (ou lève une erreur en mode strict)

this n’est pas déterminé là où une fonction est écrite — il est déterminé par la façon dont la fonction est appelée. Appelée en tant que user.greet(), l’objet au site d’appel est user, donc this.name vaut "Ada". Assignée à fn et appelée directement, il n’y a pas d’objet au site d’appel, donc this est l’objet global en mode non strict (rendant this.name undefined) ou undefined en mode strict (rendant this.name une erreur). Les quatre règles de binding sont résumées dans la référence this de MDN.

Ce qu’ils évaluent vraiment : si vous savez que this est dynamique et déterminé par le site d’appel, et non fixé lexicalement.

Question de suivi probable : « Comment ancrez-vous this à user ? » — fn.call(user), fn.apply(user), ou user.greet.bind(user).

Forme d’appelÀ quoi this est lié
fn() (appel direct)undefined (strict) / objet global (non strict)
obj.fn() (méthode)obj (l’objet à gauche du point)
fn.call(o) / fn.apply(o) / fn.bind(o)o (explicite)
new Fn()l’instance fraîchement créée
Fonction fléchéehérité de la portée lexicale englobante

8. this dans un callback

const timer = {
  seconds: 0,
  startBroken() {
    setInterval(function () { this.seconds++; }, 1000); // this est incorrect
  },
  startFixed() {
    setInterval(() => { this.seconds++; }, 1000); // this est timer
  },
};

Dans startBroken, la function ordinaire passée à setInterval est appelée par le mécanisme du timer sans objet au site d’appel, donc this n’est pas timerthis.seconds++ mute le mauvais objet. Dans startFixed, la fonction fléchée n’a pas de this propre et l’hérite de la portée de startFixed, où this est timer. C’est la raison d’être des fonctions fléchées pour les callbacks. Voir MDN sur les fonctions fléchées.

Ce qu’ils évaluent vraiment : si vous comprenez le this lexical et pourquoi les fonctions fléchées ont résolu l’ancienne solution de contournement var self = this. Le bug du this perdu dans les gestionnaires apparaît constamment en production dans les composants de classe et les écouteurs d’événements.

Question de suivi probable : « Pourquoi ne peut-on pas utiliser une fonction fléchée comme constructeur ou comme méthode d’objet nécessitant son propre this ? » — parce qu’elle n’a pas de liaison this à assigner, donc new lève une erreur et une fonction fléchée au niveau d’une méthode capture le this extérieur au lieu de l’objet.

Questions d’entretien JavaScript sur le modèle d’exécution

9. Prédisez l’ordre d’affichage dans la console (event loop)

console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Résultat : 1 4 3 2

Les callbacks de Promise (microtâches) s’exécutent toujours avant les callbacks de setTimeout (macrotâches), même avec un délai de 0ms. La trace d’exécution :

  1. console.log("1") s’exécute de façon synchrone → 1.
  2. setTimeout planifie son callback dans la file des macrotâches.
  3. Promise.resolve().then(...) planifie son callback dans la file des microtâches.
  4. console.log("4") s’exécute de façon synchrone → 4.
  5. La pile d’appels est maintenant vide. Le moteur vide l’intégralité de la file des microtâches avant toute macrotâche → 3.
  6. Seulement alors récupère-t-il la prochaine macrotâche → 2.

Le guide sur les microtâches de MDN décrit cet ordonnancement.

Ce qu’ils évaluent vraiment : si vous comprenez que l’event loop dispose de deux niveaux de priorité, et non d’une seule file. L’event loop à deux niveaux est le modèle derrière chaque bug du type « mon état s’est mis à jour un tick trop tard ».

Question de suivi probable : « Où async/await s’inscrit-il dans ce modèle ? » — le code après un await s’exécute comme une microtâche, donc il est mis en file à la même priorité qu’un callback .then().

10. == vs === et la coercition

0 == "0";        // true
0 === "0";       // false
null == undefined; // true
NaN === NaN;     // false
[] == ![];       // true

== effectue une coercition de type avant la comparaison tandis que === ne le fait pas, c’est pourquoi 0 == "0" est true mais 0 === "0" est false. Les cas contre-intuitifs : null == undefined est true par une règle spéciale (et ils ne sont égaux à rien d’autre avec ==) ; NaN n’est jamais égal à quoi que ce soit, y compris lui-même ; et [] == ![] est true parce que ![] vaut false, qui est converti en 0, et [] est converti en "" puis en 0. L’algorithme complet se trouve dans le guide sur les comparaisons d’égalité de MDN.

Ce qu’ils évaluent vraiment : si vous pouvez prédire la coercition — pas si vous pouvez réciter « toujours utiliser === ». L’interviewer veut le mécanisme, puis la règle générale.

Question de suivi probable : « Qu’en est-il de l’assignation à une variable non déclarée ? » — si value = 42 (sans var/let/const) lève une erreur ou crée silencieusement une variable globale dépend du mode : le mode strict et les modules ES lèvent une ReferenceError ; les scripts en mode non strict créent un global implicite. Même snippet, deux réponses correctes — signaler cette distinction est en soi un signal de niveau senior.

Ce qui distingue un candidat retenu d’un candidat recalé

Le fil conducteur de ces dix questions : les interviewers évaluent l’explication, pas seulement le résultat. Prédire 3 3 3 ou 1 4 3 2 prouve que vous avez déjà vu le problème ; nommer le modèle de liaison, la chaîne de portées, la règle du site d’appel et l’event loop à deux niveaux prouve que vous comprenez suffisamment le langage pour le déboguer sous pression. Travaillez ces dix questions jusqu’à pouvoir énoncer le pourquoi en deux phrases sans notes — et comme aucun de ces comportements ne dépend d’une version, vous pouvez vérifier chaque snippet vous-même dans tout moteur actuel et faire confiance au résultat.

FAQ

Quelle est la différence entre la Temporal Dead Zone et une variable simplement indéfinie ?

Une variable dans la Temporal Dead Zone a été hissée mais pas encore initialisée, donc la lire lève une ReferenceError, tandis qu'une variable var indéfinie a été hissée et initialisée à la valeur undefined, donc la lire retourne undefined. Seuls let et const créent une TDZ ; elle s'étend de l'entrée dans la portée jusqu'à l'exécution de la ligne de déclaration. La distinction est entre la création d'une liaison et son initialisation.

Pourquoi les fonctions fléchées ne fonctionnent-elles pas comme méthodes d'objet ou constructeurs ?

Les fonctions fléchées n'ont pas de liaison this propre, elles ne peuvent donc pas remplir les rôles qui en nécessitent une. Utilisée comme méthode d'objet, une fonction fléchée capture this depuis la portée lexicale englobante plutôt que depuis l'objet, donc this.property ne pointe pas vers l'objet. Utilisée avec new, le moteur lève une TypeError parce qu'il n'y a pas de liaison this à laquelle assigner la nouvelle instance. Utilisez des fonctions régulières dans les deux cas.

Le bug du setTimeout dans une boucle se comporte-t-il de la même façon dans tous les moteurs JavaScript ?

Oui. La version avec var affiche la valeur finale de la boucle dans tous les moteurs actuels parce que var crée une seule liaison à portée de fonction partagée par tous les callbacks, et la version avec let affiche les valeurs par itération partout parce que la spécification définit une nouvelle liaison par itération pour let dans une boucle for. Ce comportement est défini par la spécification dans ECMA-262, et non spécifique à un moteur, donc Node.js, V8, SpiderMonkey et JavaScriptCore produisent tous un résultat identique.

async et await modifient-ils la priorité dans l'event loop par rapport à Promise.then ?

Non. Le code après un await s'exécute comme une microtâche, à exactement la même priorité qu'un callback Promise.then, donc il s'exécute avant toute macrotâche setTimeout. Un await suspend effectivement la fonction et planifie la continuation dans la file des microtâches lorsque la valeur attendue est résolue. Cela signifie qu'une continuation attendue et un callback then mis en file au même moment se résolvent dans l'ordre du code source, et les deux s'exécutent avant tout callback de timer défini à zéro milliseconde.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.