JavaScript : Maitriser this dans les Callbacks et Event Listeners

Resolvez le probleme du this indefini en JavaScript : bind, arrow functions, closure et handleEvent pour des callbacks robustes.

Mahmoud DEVO
Mahmoud DEVO
December 27, 2025 11 min read
JavaScript : Maitriser this dans les Callbacks et Event Listeners

Introduction : Pourquoi this est problematique en JavaScript

Le mot-cle this en JavaScript est l’une des sources de confusion les plus frequentes pour les developpeurs, qu’ils soient debutants ou experimentes. Contrairement a d’autres langages de programmation comme Java ou C# ou this fait toujours reference a l’instance de la classe courante, en JavaScript, la valeur de this est determinee dynamiquement au moment de l’execution de la fonction.

Cette particularite devient particulierement problematique lorsque nous travaillons avec des callbacks, des event listeners ou des fonctions asynchrones. Le contexte d’execution change, et soudainement, this ne pointe plus vers l’objet attendu.

Dans cet article approfondi, nous allons explorer en detail :

  • Le mecanisme de resolution de this : comment JavaScript determine la valeur de this
  • Les differentes solutions pour preserver le contexte : bind(), arrow functions, closures et handleEvent
  • Les cas d’usage concrets : DOM events, timers, methodes de tableau, classes ES6
  • Les bonnes pratiques et les pieges a eviter

A la fin de cette lecture, vous aurez une comprehension solide de this et serez capable de choisir la meilleure solution selon votre contexte.


Le probleme explique : Comment JavaScript determine this

Les 4 regles de binding de this

JavaScript utilise quatre regles pour determiner la valeur de this, appliquees dans cet ordre de priorite :

1. New Binding (priorite la plus haute)

Lorsqu’une fonction est appelee avec l’operateur new, this fait reference au nouvel objet cree.

function Person(name) {
  this.name = name;
  console.log(this); // Person { name: 'Alice' }
}

const alice = new Person('Alice');

2. Explicit Binding (call, apply, bind)

Lorsqu’une fonction est appelee avec call(), apply() ou bind(), this est explicitement defini.

function greet() {
  console.log(`Bonjour, je suis ${this.name}`);
}

const user = { name: 'Bob' };
greet.call(user);  // "Bonjour, je suis Bob"
greet.apply(user); // "Bonjour, je suis Bob"

const boundGreet = greet.bind(user);
boundGreet(); // "Bonjour, je suis Bob"

3. Implicit Binding (contexte de l’objet)

Lorsqu’une fonction est appelee comme methode d’un objet, this fait reference a cet objet.

const obj = {
  name: 'Charlie',
  greet() {
    console.log(`Bonjour, je suis ${this.name}`);
  }
};

obj.greet(); // "Bonjour, je suis Charlie"

4. Default Binding (priorite la plus basse)

En mode strict, this est undefined. En mode non-strict, this fait reference a l’objet global (window dans le navigateur, global dans Node.js).

function showThis() {
  'use strict';
  console.log(this); // undefined
}

showThis();

Le probleme avec les callbacks

Le probleme survient lorsque nous passons une methode d’objet comme callback. La fonction perd son contexte d’origine :

const counter = {
  count: 0,
  increment() {
    this.count++;
    console.log(this.count);
  }
};

// Appel direct : fonctionne
counter.increment(); // 1

// Passage en callback : PROBLEME !
const btn = document.getElementById('myButton');
btn.addEventListener('click', counter.increment);
// Au clic : NaN (car this.count est undefined.count++)

Dans l’exemple ci-dessus, lorsque increment est passee en callback, elle perd sa liaison avec counter. Au moment de l’execution, this fait reference a l’element DOM qui a declenche l’evenement (le bouton), et non a l’objet counter.


Les solutions detaillees

Solution 1 : La variable self ou that (methode historique)

Avant ES6, la methode la plus courante etait de sauvegarder la reference a this dans une variable locale, generalement nommee self ou that.

function SomeClass(msg, elem) {
  var self = this; // Sauvegarde de la reference
  this.msg = msg;

  elem.addEventListener('click', function() {
    // Utilisation de self au lieu de this
    console.log(self.msg);
  });
}

var s = new SomeClass("hello", someElement);

Comment ca fonctionne ?

La variable self est capturee par la closure de la fonction callback. Meme si this change a l’interieur du callback, self garde sa valeur originale car elle fait partie du scope englobant.

const api = {
  data: [],

  fetchData() {
    const that = this;

    fetch('/api/data')
      .then(function(response) {
        return response.json();
      })
      .then(function(json) {
        // that.data au lieu de this.data
        that.data = json;
        console.log('Donnees chargees:', that.data.length);
      });
  }
};

Avantages :

  • Fonctionne dans tous les environnements, meme les plus anciens
  • Simple a comprendre et a implementer

Inconvenients :

  • Necessite une variable supplementaire
  • Peut rendre le code moins lisible
  • Considere comme une pratique obsolete depuis ES6

Solution 2 : Function.prototype.bind()

La methode bind() cree une nouvelle fonction avec un this lie de maniere permanente a la valeur specifiee.

function SomeClass(msg, elem) {
  this.msg = msg;

  elem.addEventListener('click', function() {
    console.log(this.msg);
  }.bind(this)); // Liaison permanente de this
}

Syntaxe complete de bind() :

const boundFunction = originalFunction.bind(thisArg, arg1, arg2, ...);

bind() peut egalement pre-remplir des arguments (partial application) :

function multiply(a, b) {
  return a * b;
}

const double = multiply.bind(null, 2);
console.log(double(5)); // 10
console.log(double(10)); // 20

Exemple avance avec une classe :

class UserController {
  constructor(name) {
    this.name = name;
    this.clickCount = 0;

    // Binding dans le constructeur
    this.handleClick = this.handleClick.bind(this);
    this.handleKeyPress = this.handleKeyPress.bind(this);
  }

  handleClick(event) {
    this.clickCount++;
    console.log(`${this.name} a clique ${this.clickCount} fois`);
  }

  handleKeyPress(event) {
    console.log(`${this.name} a appuye sur: ${event.key}`);
  }

  attachEvents(element) {
    element.addEventListener('click', this.handleClick);
    element.addEventListener('keypress', this.handleKeyPress);
  }

  detachEvents(element) {
    // Important: on peut retirer les listeners car ce sont les memes references
    element.removeEventListener('click', this.handleClick);
    element.removeEventListener('keypress', this.handleKeyPress);
  }
}

Avantages :

  • Crée une nouvelle fonction avec this fixe
  • Permet la pre-application d’arguments
  • La fonction bindee peut etre utilisee avec removeEventListener

Inconvenients :

  • Crée une nouvelle fonction a chaque appel (sauf si sauvegardee)
  • Legere surcharge memoire

Solution 3 : Les Arrow Functions (lexical this)

Les arrow functions, introduites avec ES6, ne possedent pas leur propre this. Elles heritent du this du scope englobant au moment de leur definition (lexical scoping).

function SomeClass(msg, elem) {
  this.msg = msg;

  elem.addEventListener('click', () => {
    // this fait reference au scope englobant (l'instance SomeClass)
    console.log(this.msg);
  });
}

Difference fondamentale avec les fonctions classiques :

const obj = {
  name: 'Demo',

  // Methode classique : this depend de l'appel
  regularMethod: function() {
    console.log('Regular:', this.name);

    setTimeout(function() {
      console.log('setTimeout regular:', this.name); // undefined !
    }, 100);
  },

  // Avec arrow function dans setTimeout
  arrowMethod: function() {
    console.log('Arrow:', this.name);

    setTimeout(() => {
      console.log('setTimeout arrow:', this.name); // 'Demo'
    }, 100);
  }
};

obj.regularMethod();
obj.arrowMethod();

Utilisation dans les classes ES6 :

class TodoList {
  constructor() {
    this.items = [];
    this.render = this.render.bind(this); // Pour les methodes classiques
  }

  // Arrow function comme propriete de classe
  addItem = (item) => {
    this.items.push(item);
    this.render();
  }

  removeItem = (index) => {
    this.items.splice(index, 1);
    this.render();
  }

  render() {
    console.log('Items:', this.items);
  }
}

const todo = new TodoList();
const btn = document.getElementById('addBtn');
btn.addEventListener('click', () => todo.addItem('Nouvelle tache'));

Avantages :

  • Syntaxe concise et lisible
  • Pas besoin de bind() ou de variable intermediaire
  • Comportement previsible (toujours le this lexical)

Inconvenients :

  • Ne peut pas etre utilisee comme constructeur (new)
  • Ne peut pas acceder a arguments (utiliser rest parameters)
  • Pas de prototype propre

Solution 4 : L’interface handleEvent

Cette solution meconnue exploite une fonctionnalite du DOM : lorsqu’un objet avec une methode handleEvent est passe a addEventListener, cette methode est appelee avec l’objet comme contexte this.

class ClickHandler {
  constructor(element, message) {
    this.message = message;
    this.element = element;
    this.clickCount = 0;
  }

  handleEvent(event) {
    // this fait automatiquement reference a l'instance
    if (event.type === 'click') {
      this.clickCount++;
      console.log(`${this.message} - Clic #${this.clickCount}`);
    } else if (event.type === 'mouseenter') {
      console.log('Souris entree');
    }
  }

  attach() {
    // On passe l'objet, pas une fonction
    this.element.addEventListener('click', this);
    this.element.addEventListener('mouseenter', this);
  }

  detach() {
    this.element.removeEventListener('click', this);
    this.element.removeEventListener('mouseenter', this);
  }
}

const handler = new ClickHandler(
  document.getElementById('myBtn'),
  'Bouton clique'
);
handler.attach();

Pattern avance avec delegation d’evenements :

class EventRouter {
  constructor() {
    this.handlers = {
      click: this.onClick,
      submit: this.onSubmit,
      input: this.onInput
    };
  }

  handleEvent(event) {
    const handler = this.handlers[event.type];
    if (handler) {
      handler.call(this, event);
    }
  }

  onClick(event) {
    console.log('Click gere:', event.target);
  }

  onSubmit(event) {
    event.preventDefault();
    console.log('Formulaire soumis');
  }

  onInput(event) {
    console.log('Input modifie:', event.target.value);
  }
}

Avantages :

  • Pas de creation de nouvelles fonctions
  • Nettoyage facile avec removeEventListener
  • Gestion centralisee de plusieurs types d’evenements

Inconvenients :

  • Moins connu, peut surprendre d’autres developpeurs
  • Specifique aux evenements DOM

Cas d’usage courants

Event Listeners DOM

class FormValidator {
  constructor(form) {
    this.form = form;
    this.errors = [];

    // Methode 1 : bind dans le constructeur
    this.onSubmit = this.onSubmit.bind(this);

    // Methode 2 : arrow function comme propriete
    // this.onInput = (e) => { ... }
  }

  onSubmit(event) {
    event.preventDefault();
    this.errors = [];
    this.validateFields();

    if (this.errors.length === 0) {
      this.submitForm();
    }
  }

  onInput = (event) => {
    // Arrow function : this est automatiquement l'instance
    this.validateField(event.target);
  }

  validateField(field) {
    if (!field.value.trim()) {
      this.errors.push(`${field.name} est requis`);
    }
  }

  validateFields() {
    const fields = this.form.querySelectorAll('input[required]');
    fields.forEach(field => this.validateField(field));
  }

  submitForm() {
    console.log('Formulaire valide, soumission...');
  }

  init() {
    this.form.addEventListener('submit', this.onSubmit);
    this.form.querySelectorAll('input').forEach(input => {
      input.addEventListener('input', this.onInput);
    });
  }
}

setTimeout et setInterval

class Timer {
  constructor(duration) {
    this.duration = duration;
    this.remaining = duration;
    this.intervalId = null;
  }

  // ERREUR COURANTE - this sera window/undefined
  startBroken() {
    this.intervalId = setInterval(function() {
      this.remaining--; // TypeError: Cannot read property 'remaining' of undefined
      console.log(this.remaining);
    }, 1000);
  }

  // SOLUTION 1 : Arrow function
  start() {
    this.intervalId = setInterval(() => {
      this.remaining--;
      console.log(`Temps restant: ${this.remaining}s`);

      if (this.remaining <= 0) {
        this.stop();
        this.onComplete();
      }
    }, 1000);
  }

  // SOLUTION 2 : Methode bindee
  tick = () => {
    this.remaining--;
    console.log(`Temps restant: ${this.remaining}s`);

    if (this.remaining <= 0) {
      this.stop();
      this.onComplete();
    }
  }

  startWithBind() {
    this.intervalId = setInterval(this.tick, 1000);
  }

  stop() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }

  reset() {
    this.stop();
    this.remaining = this.duration;
  }

  onComplete() {
    console.log('Timer termine !');
  }
}

const timer = new Timer(10);
timer.start();

Methodes de tableau (map, filter, forEach)

Les methodes de tableau comme map, filter, forEach, reduce acceptent un second argument optionnel pour definir this :

class DataProcessor {
  constructor(data) {
    this.data = data;
    this.multiplier = 2;
    this.threshold = 10;
  }

  // SOLUTION 1 : Arrow functions (recommande)
  processWithArrow() {
    return this.data
      .filter(item => item > this.threshold)
      .map(item => item * this.multiplier);
  }

  // SOLUTION 2 : Second argument thisArg
  processWithThisArg() {
    return this.data
      .filter(function(item) {
        return item > this.threshold;
      }, this) // <-- thisArg
      .map(function(item) {
        return item * this.multiplier;
      }, this); // <-- thisArg
  }

  // SOLUTION 3 : bind()
  processWithBind() {
    const filterFn = function(item) {
      return item > this.threshold;
    }.bind(this);

    const mapFn = function(item) {
      return item * this.multiplier;
    }.bind(this);

    return this.data.filter(filterFn).map(mapFn);
  }

  // Exemple avec forEach
  logItems() {
    this.data.forEach(function(item, index) {
      console.log(`Item ${index}: ${item * this.multiplier}`);
    }, this);
  }
}

const processor = new DataProcessor([5, 10, 15, 20, 25]);
console.log(processor.processWithArrow()); // [30, 40, 50]

this dans les classes ES6

Methodes de classe et binding automatique

En ES6, les methodes de classe ne sont pas automatiquement bindees a l’instance. C’est un comportement intentionnel pour des raisons de performance.

class Button {
  constructor(label) {
    this.label = label;
    this.clickCount = 0;
  }

  // Methode classique : this n'est pas binde automatiquement
  handleClick() {
    this.clickCount++;
    console.log(`${this.label} clique ${this.clickCount} fois`);
  }
}

const btn = new Button('Mon bouton');
const element = document.getElementById('btn');

// PROBLEME : handleClick perd son contexte
element.addEventListener('click', btn.handleClick); // this sera l'element DOM

// SOLUTIONS :
// 1. bind() dans addEventListener
element.addEventListener('click', btn.handleClick.bind(btn));

// 2. Arrow function wrapper
element.addEventListener('click', (e) => btn.handleClick(e));

// 3. bind() dans le constructeur (voir exemple suivant)

Proprietes de classe avec arrow functions

La syntaxe des proprietes de classe (class fields) permet de definir des methodes comme arrow functions, garantissant un binding automatique :

class ModernButton {
  constructor(label) {
    this.label = label;
    this.clickCount = 0;
  }

  // Propriete de classe avec arrow function
  // this est automatiquement lie a l'instance
  handleClick = (event) => {
    this.clickCount++;
    console.log(`${this.label} clique ${this.clickCount} fois`);
    console.log('Element cible:', event.target);
  }

  handleDoubleClick = () => {
    console.log(`Double-clic sur ${this.label}`);
  }

  // Methode classique pour les operations qui n'ont pas besoin d'etre passees en callback
  render() {
    return `<button>${this.label}</button>`;
  }
}

const modernBtn = new ModernButton('Bouton moderne');
const el = document.getElementById('btn');

// Plus besoin de bind() !
el.addEventListener('click', modernBtn.handleClick);
el.addEventListener('dblclick', modernBtn.handleDoubleClick);

Comparaison des deux approches :

// Approche 1 : bind() dans le constructeur
class ApproachOne {
  constructor() {
    this.value = 42;
    this.getValue = this.getValue.bind(this);
  }

  getValue() {
    return this.value;
  }
}

// Approche 2 : Proprietes de classe avec arrow functions
class ApproachTwo {
  value = 42;

  getValue = () => {
    return this.value;
  }
}

// Les deux fonctionnent de la meme maniere
const obj1 = new ApproachOne();
const obj2 = new ApproachTwo();

const fn1 = obj1.getValue;
const fn2 = obj2.getValue;

console.log(fn1()); // 42
console.log(fn2()); // 42

Bonnes Pratiques

1. Preferez les arrow functions pour les callbacks inline

// BON : Arrow function inline
element.addEventListener('click', () => {
  this.handleInteraction();
});

// A EVITER : bind() inline (cree une nouvelle fonction a chaque rendu)
element.addEventListener('click', this.handleInteraction.bind(this));

2. Utilisez bind() dans le constructeur pour les methodes reutilisees

class Component {
  constructor() {
    // Bind une seule fois dans le constructeur
    this.handleClick = this.handleClick.bind(this);
    this.handleKeyDown = this.handleKeyDown.bind(this);
  }

  handleClick(event) { /* ... */ }
  handleKeyDown(event) { /* ... */ }

  mount(element) {
    // Les memes references peuvent etre utilisees pour add et remove
    element.addEventListener('click', this.handleClick);
    element.addEventListener('keydown', this.handleKeyDown);
  }

  unmount(element) {
    element.removeEventListener('click', this.handleClick);
    element.removeEventListener('keydown', this.handleKeyDown);
  }
}

3. Utilisez les proprietes de classe pour les handlers d’evenements

class ModernComponent {
  // Proprietes de classe avec arrow functions
  onClick = (e) => this.processClick(e);
  onSubmit = (e) => this.processSubmit(e);

  processClick(event) { /* logique */ }
  processSubmit(event) { /* logique */ }
}

4. Documentez clairement quand une methode doit etre bindee

class Service {
  /**
   * Callback pour les requetes API
   * @note Cette methode doit etre bindee si utilisee comme callback
   */
  onDataReceived(data) {
    this.processData(data);
  }

  // Alternative : marquer clairement les handlers
  handleDataReceived = (data) => {
    this.processData(data);
  }
}

5. Evitez de mixer les approches dans une meme classe

// A EVITER : Incoherent
class InconsistentComponent {
  constructor() {
    this.method1 = this.method1.bind(this);
  }
  method1() { }
  method2 = () => { }
  method3() { } // Non binde !
}

// BON : Coherent
class ConsistentComponent {
  onClick = () => { }
  onSubmit = () => { }
  onInput = () => { }
}

6. Utilisez handleEvent pour les composants avec beaucoup d’evenements

class RichComponent {
  handleEvent(event) {
    const method = this[`on${event.type.charAt(0).toUpperCase()}${event.type.slice(1)}`];
    if (method) method.call(this, event);
  }

  onClick(e) { console.log('click'); }
  onMouseenter(e) { console.log('mouseenter'); }
  onMouseleave(e) { console.log('mouseleave'); }
  onFocus(e) { console.log('focus'); }
  onBlur(e) { console.log('blur'); }

  attach(el) {
    ['click', 'mouseenter', 'mouseleave', 'focus', 'blur'].forEach(type => {
      el.addEventListener(type, this);
    });
  }
}

Pieges Courants

1. Le double bind inutile

// ERREUR : Double bind ne sert a rien
const handler = this.handleClick.bind(this).bind(this);

// Le premier bind() cree une fonction avec this fixe
// Les appels subsequents a bind() sont ignores

2. bind() dans les proprietes JSX/render (performance)

// MAUVAIS : Cree une nouvelle fonction a chaque rendu
class BadComponent {
  render() {
    return (
      <button onClick={this.handleClick.bind(this)}>
        Cliquer
      </button>
    );
  }
}

// BON : bind() dans le constructeur ou arrow function
class GoodComponent {
  constructor() {
    this.handleClick = this.handleClick.bind(this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Cliquer
      </button>
    );
  }
}

3. Oublier que les arrow functions n’ont pas leur propre this

const obj = {
  name: 'Object',

  // ATTENTION : this sera celui du scope englobant, pas obj
  arrowMethod: () => {
    console.log(this.name); // undefined (ou window.name en mode non-strict)
  },

  // BON : Methode classique pour les methodes d'objet
  regularMethod() {
    console.log(this.name); // 'Object'
  }
};

4. this dans les modules ES6

// module.js
// En mode strict (modules sont toujours en strict mode)
console.log(this); // undefined

export function standalone() {
  console.log(this); // undefined en appel direct
}

export const obj = {
  method() {
    console.log(this); // obj quand appele comme obj.method()
  }
};

5. Perdre la reference lors de la destructuration

const service = {
  data: [1, 2, 3],
  process() {
    return this.data.map(x => x * 2);
  }
};

// PROBLEME : this est perdu
const { process } = service;
process(); // TypeError: Cannot read property 'data' of undefined

// SOLUTION
const boundProcess = service.process.bind(service);
boundProcess(); // [2, 4, 6]

Conclusion et tableau comparatif

Le mot-cle this en JavaScript est un concept fondamental mais delicat. Comprendre comment il fonctionne et connaitre les differentes techniques pour le controler est essentiel pour ecrire du code robuste et maintenable.

Tableau comparatif des solutions

SolutionSyntaxeCas d’usageAvantagesInconvenients
Variable self/thatconst self = thisLegacy code, compatibiliteSimple, universelObsolete, verbeux
bind().bind(this)Methodes reutilisables, removeEventListenerFlexibilite, pre-application argsCree nouvelle fonction
Arrow function() => {}Callbacks inline, proprietes de classeSyntaxe concise, this lexicalPas de constructeur, pas d’arguments
handleEventaddEventListener(type, obj)Composants DOM complexesZero allocation, multi-eventsSpecifique au DOM

Recommandations par contexte

  • Callbacks inline simples : Arrow functions
  • Methodes de classe reutilisees : bind() dans le constructeur ou proprietes de classe
  • Composants avec beaucoup d’evenements DOM : Interface handleEvent
  • Code legacy ou compatibilite maximale : Variable self/that

Points cles a retenir

  1. this est determine au moment de l’appel, pas de la definition (sauf arrow functions)
  2. Les arrow functions heritent du this lexical
  3. bind() cree une nouvelle fonction avec this fixe
  4. Utilisez une approche coherente dans votre codebase
  5. Attention aux pieges de performance (bind inline dans render)

Avec ces connaissances, vous etes maintenant equipes pour gerer efficacement this dans tous vos projets JavaScript, que ce soit pour des applications front-end, back-end Node.js, ou des bibliotheques.

Advertisement

In-Article Ad

Dev Mode

Share this article

Mahmoud DEVO

Mahmoud DEVO

Senior Full-Stack Developer

I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.

Enjoyed this article?

Subscribe to get more tech content delivered to your inbox.

Related Articles