JavaScript et this

Souvent décrié, parfois mal compris, le this en JavaScript peut en dérouter plus d’un car ce langage ne manque pas de subtilités.

Attention au biais

Dès qu’on essaie de plaquer un concept vu dans un certain langage, php par exemple, avec un langage tel que JavaScript :

 PAF, c’est le platane assuré en revenant de Louvin-la-neuve.

Essayons d’éviter la sortie de route.

$this en php

Pour aller vite, disons que $this fait référence à un objet instancié. Concrètement, ça ressemble à :

<?php 
class SimpleClass
{
    // déclaration d'une propriété
    public $var = 'une valeur par défaut';

    // déclaration des méthodes
    public function displayVar() {
        echo $this->var;
    }
}

chaque fois qu’on va instancier la classe et appeler notre méthode :

<?php
$newObject = new SimpleClass;
$newObject->displayVar();

le $this fera référence, la plupart du temps, à notre nouvel objet fraîchement instancié et sera donc disponible à l’intérieur de l’objet lui-même.

Chaque instanciation produisant un nouvel objet, si je fais :

$newObject = new SimpleClass;
$newObject->var = 'toto';// on peut le faire car la propriété est publique ici
$newObject->displayVar();

displayVar() affichera “toto” et non “une valeur par défaut” de même que $this->var vaudra “toto” à l’intérieur de la classe.

Source

this en JavaScript

Les problèmes commencent si on se met à utiliser this comme on vient de le faire en php. Mais pourquoi ?

Non pas qu’il n’existe pas d’objet ou de classes en JavaScript et que this à l’intérieur d’une classe js ne fasse pas référence à l’objet en cours, non, cela est aussi valable en js, mais cela n’est pas que ça.

this dépend du contexte d’exécution et tant que ce contexte s’exécute il sera déterminé par ce dernier.

Contexte global

Pas vraiment de surprise dans ce contexte en fait.

this.toto = 'calo';// this fait référence à l'objet window
console.log(this.toto);// affiche "calo"

À noter que le code suivant fonctionne :

var toto = 'calo';
console.log(this.toto);// affiche "calo"

tout comme :

toto = 'calo';
console.log(this.toto);// affiche "calo"

En fait quand je dis “pas vraiment de surprise”, il faut quand même éviter l’erreur classique :

var config = 'xxxx';
function itWasAtThisMomentHeKnewHeFuckedUp() {
    config = 'yyyy';
}
itWasAtThisMomentHeKnewHeFuckedUp();
console.log(this.config);//affiche "yyyy"

la variable étant globale (window), la modifier dans une fonction la modifie partout !

C’est pour ça qu’on fera plutôt :

var config = 'xxxx';
(function() {
    'use strict';
    function everythingIsOK() {
        var config = 'yyyy';
    }
everythingIsOK();
})();
console.log(this.config);//affiche "xxxx"

en entourant avec une IIFE (fonction anonyme auto-appelante).

Contexte local

Suivant la façon dont sera appelée la fonction, this pourra changer.

Autrement dit, si on se place dans un event comme le “on click”, this n’est plus l’objet window global mais l’objet cliqué (en partant du principe que vous avez un bouton dont l’ID est “myButton” dans la page) :

function debugThis() {
    console.log(this);
}
var el = document.getElementById("myButton");
el.addEventListener("click", debugThis, false);

ici this renverra bien l’objet qui correspond au bouton. Une autre façon de l’écrire serait :

var el = document.getElementById("myButton");
el.addEventListener("click", function () {
    console.log(this);
}, false);

Source

Cela veut bien dire que suivant la manière dont la fonction est appelée la valeur de this change.

Les fonctions fléchées

Dans une fonction fléchée, comme l’indique bien la documentation :

absence de this spécifique à la fonction.

Source

donc le contexte (et donc la valeur de this) sera celui dans lequel est définie la fonction.

Dans une classe

class Rectangle {
  constructor(hauteur, largeur) {
    this.hauteur = hauteur;
    this.largeur = largeur;
  }
}

Ici this est lié à l’objet en construction.

Le mode strict

Le mode strict permet de capter certaines erreurs parmi lesquelles modifier une variable globale ce qui peut être très utile lors du développement d’une application. Il empêche également l’utilisation de fonctionnalités jugées sources de confusion ou dangereuses.

Les développeurs(ses) JavaScript recommandent son utilisation systématique, à mettre au début de vos fichiers et au début de vos fonctions :

function myFunction(){
    "use strict"; 
}

Techniquement ce mode fixe la valeur de this telle qu’elle était au moment de la définition, peu importe le contexte d’exécution.

Le contexte bloc (block scope)

Pour aller vraiment vite, on peut dire que le block scope est défini par des {} et on le retrouve par exemple lorsqu’on fait des if, des for, etc :

for (var i=0; i<73; i++) {
  // contexte bloc
}

Seulement attention les variables déclarées dans ce contexte continuent à vivre après. Si vous faites :

console.log(i);//affiche 73

après la boucle for vous obtiendrez “73” !

Photo par Ben White sur Unsplash

ES6 à la rescousse ?

Depuis ES6 on peut utiliser const et let au lieu de var. Contrairement à var ils ne sont pas accessibles de l’extérieur.

En ce qui concerne let si on reprend l’exemple de la boucle for :

for (let i=0; i<73; i++) {
//contexte bloc
}
console.log(i);// erreur !

ici on aura une erreur contrairement à tout à l’heure car la variable n’existe plus en dehors de la boucle.

Pour ce qui est de const c’est le même fonctionnement excepté qu’on ne peut pas modifier la constante après l’avoir déclaré une première fois. C’est la raison pour laquelle on voit souvent l’import de librairies ou de composants externes dans des constantes :

const myLibrary = require('MYLIBRARY');

Les dév js aiment bien aussi déclarer leurs fonctions avec des constantes car il n’y a pratiquement pas de cas où il auront à les rédéfinir.

Le scope chain

Les scopes, ou les contextes si vous préférez, peuvent s’imbriquer.

En js, on l’a rapidement vu dans les exemples mis en avant, on peut imbriquer des fonctions dans des fonctions :

function myFunction() {
  // contexte local
  function myNestedFunction() {
    // contexte local imbriqué
  }
}

Les scopes peuvent donc être imbriqués, on aura alors un contexte parent et un contexte enfant. Seul le scope global n’a pas de parent évidemment puisqu’il est tout en haut de la pile.

Pour connaître le contexte, faites appel à this :

console.log(this);

sa valeur vous le donnera.

Le scope chain c’est le fait que tout scope, en dehors du scope global, est relié à d’autres scopes. Ces scopes sont imbriqués entre eux et forment au final une hiérarchie.

Dans la technique, le moteur de JavaScript fonctionne à peu près ainsi : quand il rencontre la fonction myFunction() appartenant au contexte global, il ajoute le scope de cette fonction dans sa liste de contextes d’exécution et ensuite il exécute la fonction.

Quand il rencontre la fonction myNestedFunction() il fait pareil, il ajoute le scope de la fonction dans sa liste et exécute ensuite la fonction et mais il supprime de sa liste le scope de la fonction myNestedFunction() et exécute le reste du code éventuellement présent dans la fonction myFunction(). Le scope de la fonction myFunction() se retrouve alors en haut de la liste.

Quand il a terminé avec le code de myFunction() il supprime le scope de la fonction myFunction(). Il ne reste plus que le scope global, quand tout le code de ce scope est exécuté, il supprime de sa liste le scope global et JavaScript s’arrête.

Source

Comprendre cette notion de scope chain permet d’éviter certains écueils.

Conclusion

On dit souvent que this varie selon le contexte en JavaScript, on critique parfois le js pour ça. On a vu qu’effectivement, il y a certaines notions à connaître sous peine d’avoir des erreurs complexes à déboguer.

Le js fonctionne ainsi pour le moment, il faut donc le savoir. this varie mais le mode strict, les nouveaux mots-clés d’ES6 et la notion de scope chain peuvent aider à éviter les pièges.

Une bonne pratique est de faire appel à une IFFE pour écrire son code afin de ne pas “polluer” le scope global.