Table of Contents
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 dethis - Les differentes solutions pour preserver le contexte :
bind(), arrow functions, closures ethandleEvent - 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
thisfixe - 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
thislexical)
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
| Solution | Syntaxe | Cas d’usage | Avantages | Inconvenients |
|---|---|---|---|---|
| Variable self/that | const self = this | Legacy code, compatibilite | Simple, universel | Obsolete, verbeux |
| bind() | .bind(this) | Methodes reutilisables, removeEventListener | Flexibilite, pre-application args | Cree nouvelle fonction |
| Arrow function | () => {} | Callbacks inline, proprietes de classe | Syntaxe concise, this lexical | Pas de constructeur, pas d’arguments |
| handleEvent | addEventListener(type, obj) | Composants DOM complexes | Zero allocation, multi-events | Specifique 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
thisest determine au moment de l’appel, pas de la definition (sauf arrow functions)- Les arrow functions heritent du
thislexical bind()cree une nouvelle fonction avecthisfixe- Utilisez une approche coherente dans votre codebase
- 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.
In-Article Ad
Dev Mode
Tags
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
Fonctions flechees et WebSockets en JavaScript : guide complet avec exemples pratiques
Apprenez a utiliser les fonctions flechees et WebSockets en JavaScript. Connexions bidirectionnelles, messages binaires et securite HTTPS.
Manipulation des dates en JavaScript : UTC, conversion et formatage
Guide complet sur les dates JavaScript : conversion en chaine, creation de dates UTC, methodes setUTC et bonnes pratiques pour eviter les problemes de fuseaux.
La comparaison équivalente en JavaScript : une analyse appro
Here's a compelling meta description that summarizes the main value proposition, includes a subtle call-to-action, and meets the 150-160 character requirement: