Pour enfin comprendre Javascript

Javascript est un langage qui dispose de sa propre logique un peu tordue. Sa syntaxe ressemble vaguement à celles d'autres langages, mais on obtient parfois des résultats suprenants. Et puis ces erreurs incompréhensibles « this is undefined », « undefined is not a function », etc. Mais pas de paniques, suivez ce tutoriel pour bien comprendre Javascript.

Gros plan sur les céramiques du Palais Güell

Il est des technologies qui se laissent facilement apprivoiser. Prenez Python, par exemple ; sa syntaxe est claire et limpide comme de la sueur de licorne et dès la première approche, le développeur se sentira aussi à l'aise qu'un Hobbit au fond de son trou. Jetez un œil à ce bout de code Python ; on dirait du Beaudelaire, n'est-ce pas ?

class Pomodoro:
    def __init__(self, duration):
        self.duration = duration

pomodoro = Pomodoro(25)

D'autres langages, en revanche, feraient plutôt penser à de féroces étalons galopant l'écume aux dents dans les steppes de Mongolie. Ruby, par exemple :

class Pomodoro
    attr_reader :duration
    def initialize(duration)
        @duration = duration
    end
end

pomodoro = Pomodoro.new(25)

Et puis il y a Javascript. La première fois que mes yeux se sont posés sur du Javascript, j'ai immédiatement visualisé un Panda borgne courant derrière son bus avec des chaussures trop grandes. Franchement, il faut bien avouer que la comparaison paraît plutôt défavorable :

(function(exports) {
    "use strict";

    var Pomodoro = function(duration) {
        this.duration = duration;
    }
    exports.Pomodoro = Pomodoro;
})(this);

var pomodoro = new Pomodoro(25);

Pourtant, si Javascript fait partie des langages les plus utilisés aujourd'hui, c'est qu'il doit bien y avoir une raison. Si nous voulons apprendre à utiliser ce sympathique outil correctement, il nous faudra mettre de côté quelques minutes notre intégrisme ronchonnant et étudier ses entrailles un peu plus en détails.

The good parts

Dans tout langage, il y a des éléments plus ou moins réussis. Le développeur expérimenté apprend vite à laisser de côté les éléments de syntaxe et les structures de données problématiques pour se concentrer sur l'important : la lisibilité du code.

Dans Javascript, il y a beaucoup de mauvais côtés. Beaucoup de failles de conception et d'éléments qui peuvent provoquer des bugs inattendus et rendre fou le développeur insouciant. À tel point que l'un des meilleurs livres sur Javascript s'appelle « The Good Parts », par Douglas Crockford.

Apprivoiser Javascript, c'est accepter le fait qu'il y ait beaucoup d'éléments à laisser de côté pour se concentrer sur ce qui marche bien. Une fois passée cette étape, on évite les surprises désagréables.

Les types de données en Javascript

Dans Javascript, il n'y a pas 150 types de données élémentaires. On trouve :

  • les booléens (boolean) ;
  • les chaînes de caractères (string) ;
  • les nombres (number) ;
  • et les objets (Object).

Parmi les sous-types d'objets, on trouve entre autres :

  • les tableaux (Array) ;
  • les dates (Date) ;
  • les expressions régulières (RexExp) ;
  • et les fonction (Function).

Les deux structures de données les plus importantes sont les objets et les fonctions.

Les objets Javascript

Un objet est simplement une liste de valeur indexées par des clés qui sont des chaînes de caractère. Voici un très bel objet :

var blog = {
    title: 'Miximum',
    url: 'https://www.miximum.fr',
    tags: ['developpement', 'freelance', 'python', 'django', 'javascript'],
    author: {
        firstname: 'Thibault',
        lastname: 'Jouannic'
    }
};

Notez que les objets sont des types de données mutables. On peut très bien écrire après coup :

blog.description = "Le blog d'un freelance mégalo";

Les fonctions

Une fonction est elle-même un objet, avec ceci de particulier qu'on peut l'invoquer.

function get_title(blog) {
    return blog.title;
}

get_title(blog); // "Miximum"

Une fonction étant un objet, on peut l'affecter à n'importe quelle variable. D'ailleurs, on préférera la syntaxe suivante, sémantiquement équivalente, plus claire, plus élégante, plus « Javascript » :

var get_title = function(blog) {
    return blog.title;
};

En plus des arguments passés manuellement, chaque fonction en Javascript reçoit deux arguments particuliers, nommés arguments et this.

arguments contient la liste de tous les arguments passés lors de l'appel de la fonction.

var avg = function() {
    var sum = 0;
    for (var i = 0 ; i < arguments.length ; i++) {
        sum += arguments[i];
    }

    return sum / arguments.length;
};
avg(5, 6, 8, 13, 2);  // 6.8

Maintenant, this. En Javascript, si une fonction est invoquée dans le contexte d'un objet (on parle alors de méthode), alors this prend la valeur de cet objet.

var person = {
    firstName: 'Bob',
    lastName: 'Sponge',
    fullName: function() {
        return this.firstName + ' ' + this.lastName;
    }
};
person.fullName();  // Bob Sponge

C'est un peu piégeux, parce que this n'est pas intrinsèquement lié à l'objet. Si la même fonction est invoquée hors dans le context global, alors this prend la valeur de l'objet global (l'élément window dans le contexte d'un navigateur), comme l'illustre le code suivant :

var fn = person.fullName;
fn();  // undefined undefined

Bon, jusque là, rien de trop difficile. Abordons maintenant la partie qui fâche.

Les classes en Javascript

Javascript est un langage sournois. Sisi, Javascript est sournois. Pour vous en convaincre, jetez un œil à la ligne suivante :

var pomodoro = new Pomodoro(25);
console.log(this.duration);

Rien de bien extraordinaire me direz-vous. On créé un objet en instanciant la classe Pomodoro grâce au mot clé new, comme dans la plupart des langages objets.

Et c'est là que Javascript est vil. Parce que si Javascript reprend des éléments de syntaxe des langages à base de classes, le concept de classe n'existe pas en Javascript.

Il n'y a pas de classes en Javascript. Répétez après moi : « Il n'y a pas de classes en Javascript. Il n'y a pas… »

Python, Ruby, Java, etc. sont des langages objets, mais il faudrait préciser qu'il s'agit de langages à base de classes. Javascript est bien un langage objet, mais pas un langage à base de classes.

Rappel sur les classes

Ça peut paraître imbécile de revenir sur des concepts qui semblent évident au développeur ayant plus de quelques mois d'expérience, mais pour bien comprendre Javascript, il faut se demander : « à quoi sert une classe ? ».

Alors, je vous écoute ?!

Dans la plupart des langages objets, la classe est le principal outil architectural. Architecturer du code, c'est concevoir une abstraction du problème sous forme d'interactions entre plusieurs éléments, et donc découper son programme en une hiérarchie de classes. Créer des objets nécessite d'instancier des classes, et donc de définir au préalable et une fois pour toute la structure de ces objets.

Voici un très bel exemple de classe écrite en Python :

class User:
    """Represents a single user in the application."""

    PASSWORD = 'gloubiboulga'

    def __init__(self, first_name, last_name):
        """Create a new user."""
        self.firts_name = first_name
        self.last_name = last_name
        self.is_authenticated = False

    def __str__(self):
        return '%s %s' % (self.first_name, self.last_name)

    def authenticate(self, password):
        """Mark the user as authenticated."""
        if password == self.PASSWORD:
            self.is_authenticated = True


toto = User("Toto", "d'Aumont de Rochebaron")
toto.authenticate('schmilblick')
toto.is_authenticated  # False

Une classe a donc de nombreuses utilités :

  • architecturer le code ;
  • réutiliser du code ;
  • créer des abstractions facilitant la compréhension du problème ;
  • etc.

Dans la mesure ou Javacript ne propose pas le mécanisme des classes, il nous faut comprendre comment atteindre ces objectifs autrement. Nous allons voir quels sont les éléments à notre disposition pour ça.

Le pattern constructor

Puisqu'il n'y a pas de classes en Javascript, pas besoin de s'embêter ; quand on a besoin d'un objet, on le créé à la volée.

var tata = {
    firstName: 'Tata',
    lastName: 'de La Rochette de Rochegonde',
    toString: function() {
        return this.firstName + ' ' + this.lastName;
    }
};

Pourquoi faire plus complexe ? Évidemment, définir toute une série d'objets similaires deviendrait vite fastidieux, mais rien ne nous empêche de définir une fonction pour le faire à notre place.

var makeUser = function(firstName, lastName) {
    return {
        firstName: firstName,
        lastName: lastName,
        toString: function() {
            return this.firstName + ' ' + this.lastName;
        }
    };
};
var tata = makeUser('Tata', 'de La Rochette de Rochegonde');
var toto = makeUser('Toto', "d'Aumont de Rochebaron");

À vrai dire, ce pattern est tellement courant que Javascript propose une alternative syntaxique destinée à rendre l'opération plus claire.

var User = function(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.toString = function() {
        return this.firstName + ' ' + this.lastName;
    };
};
var tata = new User('Tata', 'de La Rochette de Rochegonde');

Le fait d'appeler notre fonction grâce au mot clé new va créer un objet à notre place, et le passer en paramètre this de la fonction que l'on appelle. Ce n'est pas la seule opération réalisée par new, mais nous verrons le reste plus tard.

Notez qu'il n'y a pas besoin de retourner explicitement this à la fin de la fonction, Javascript le fait pour vous (c'est magique).

Une fonction destinée à être appelée grâce à new est un constructor ; par convention son nom est défini en CamelCase.

Notez que si vous oubliez d'utiliser new en appelant votre fonction, alors this équivaudra à l'objet global, ce qui peut produire des catastrophes assez imprévisibles.

Les prototypes

Si vous êtes suffisamment réveillés, vous avez du remarquer une faille dans le code ci-dessus. En effet, le corps de la méthode toString est bel et bien dupliqué dans chaque objet instancié par User. Ce qui fait que chaque User dispose de sa propre méthode toString.

var User = function(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.toString = function() {
        return this.firstName + ' ' + this.lastName;
    };
};

var toto = new User('Toto', "d'Aumont de Rochebaron");
var tata = new User('Tata', 'de La Rochette de Rochegonde');

toto.toString = function() {
    return "I'm anonymous, bitch!";
};

tata.toString();  // Tata de La Rochette de Rochegonde
toto.toString();  // I'm anonymous, bitch!

C'est un peu embêtant, parce que si je créé quelques centaines ou milliers d'objets User, je vais monopoliser de la mémoire pour rien. Et puis, ça fait désordre.

Heureusement, Javascript propose un concept qui permet le partage de code entre plusieurs objets. Il s'agit du mécanisme des prototypes.

En Javascript, chaque objet est lié à un prototype, qui est lui même un objet tout ce qu'il y a de plus standard. Lorsque l'on tente d'accéder à une propriété d'un objet, et que cette propriété n'existe pas, alors Javascript va vérifier si la propriété existe dans le prototype de l'objet.

Par défaut, chaque objet est lié à Object.prototype, Object étant une variable globale définie au niveau du langage.

Object.prototype.toString = function() {
        return this.firstName + ' ' + this.lastName;
};

var tata = {
    firstName: 'Tata',
    lastName: 'de La Rochette de Rochegonde'
};

tata.toString();  // Tata de la Rochette de Rochegonde

Évidemment, ce n'est pas forcément une bonne idée de modifier le prototype de l'objet standard. Ce qu'il faudrait, c'est pouvoir spécifier notre propre prototype. Et bien c'est également le rôle du mot clé new. En effet, chaque fonction en Javascript dispose d'une propriété prototype — c'est possible car une fonction est bien un objet, vous vous souvenez ? Quand vous utilisez le pattern constructor pour créer un objet, celui-ci est lié au prototype de ce constructor.

var User = function(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
};

User.prototype;  // {}

User.prototype.toString = function() {
    return this.firstName + ' ' + this.lastName;
};

var tata = new User('Tata', 'de La Rochette de Rochegonde');
tata.toString();  // Tata de la Rochette de Rochegonde

Ici, j'ajoute une fonction toString dans le prototype de User ; ainsi tous les objets créés par User pourront disposer de la fonction toString via leur prototype. Cool, non ?

L'héritage prototypal

Comme les lecteurs de Miximum sont particulièrement sagaces, je suis sûr qu'une question vous brûle les lèvres.

« Si j'ai bien compris, chaque objet est lié à un prototype ?
– Exact !
– Et un prototype est un objet comme les autres ?
– Toujours exact.
– En toute logique, chaque prototype est donc lui-même lié à un prototype ?
– C'est vrai.
– Haha ! C'est donc le colonel Moutarde l'assassin !
– Heu… »

Si Javascript ne trouve pas une propriété dans un objet, il ira la chercher dans son prototype. Mais s'il ne l'y trouve pas, il ira chercher dans le prototype du prototype, etc. jusqu'au bout de la chaîne, c'est à dire jusqu'à Object.prototype. On parle d'héritage prototypal.

var toto = new User();
toto.gloubiboulga;

Ici, le chemin parcouru sera donc :

toto -> User.prototype -> Function.prototype -> Object.prototype

Nous pouvons tirer parti de ce mécanisme pour mettre un place un pseudo système d'héritage.

var AnonymousUser = function(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
};

AnonymousUser.prototype.toString = function() {
    return this.firstName + ' ' + this.lastName;
};

var AuthenticatedUser = function() {
    // Ici, on appelle manuellement le constructeur "parent"
    AnonymousUser.apply(this, arguments);
};
AuthenticatedUser.prototype = new AnonymousUser();

AuthenticatedUser.prototype.post_comment = function(comment) {
    // …
};

Vous remarquerez que l'on est obligé de créer une instance d'AnonymousUser au moment de la déclaration d'AuthenticatedUser. Wat?! C'est effectivement un peu étrange d'un point de vue conceptuel. Néanmoins, le mécanisme des prototypes fait que les objets héritent directement des propriétés d'autres objets.

On peut vérifier si un objet est lié au prototype d'un constructeur donné grâce à instanceof.

var tutu = new AuthenticatedUser('Tutu', 'Ango de La Motte-Ango de Flers');
tutu instanceof AuthenticatedUser;  // true
tutu instanceof AnonymousUser; // true
tutu instanceof Object; // true
tutu instanceof Array; // alse

C'est d'ailleurs un choix terminologique douteux puisque parler d'instance relève d'un abus de langage. Mais nous ne sommes plus à ça près.

Quoiqu'il en soit, en Javascript, raisonner en hiérarchie de classes n'est plus le seul moyen de structurer une application. On tâchera donc de privilégier d'autres patterns, e.g. la composition plutôt que l'héritage.

Modules, closures et pattern IIFE

Fort de ces nouveau concepts, tâchons de réécrire en Javascript notre classe User définie un peu plus haut en Python.

PASSWORD = 'gloubiboulga';

var User = function(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.isAuthenticated = false;
};

User.prototype.toString = function() {
    return this.firstName + ' ' + this.lastName;
};

User.prototype.authenticate = function(password) {
    if (password == PASSWORD) {
        this.isAuthenticated = true;
    }
};

var toto = new User("Toto", "d'Aumont de Rochebaron");
toto.authenticate('schmilblick');
toto.isAuthenticated;  // False

C'est un peu mieux que tout à l'heure, mais pas encore ça. En l'état, la variable PASSWORD est globale, et donc accessible depuis l'ensemble de l'application. Une solution toute simple serait d'englober notre constructeur dans une fonction.

var makeUserClass = function() {
    var PASSWORD = 'gloubiboulga';

    var User = function(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.isAuthenticated = false;
    };

    User.prototype.toString = function() {
        return this.firstName + ' ' + this.lastName;
    };

    User.prototype.authenticate = function(password) {
        if (password == PASSWORD) {
            this.isAuthenticated = true;
        }
    };

    return User;
};
User = makeUserClass();

PASSWORD fait maintenant partie du scope local à la fonction makeUserClass, et n'est donc plus accessible globalement. Un bon point.

Néanmoins, le fait que ce code puisse fonctionner semble franchement contre-intuitif. En effet, au moment ou authenticate est appelée, l'exécution de makeUserClass est depuis longtemps terminée. Il semble donc que les variables définies dans makeUserClass ne soient pas détruites même après la fin de l'exécution de la fonction.

Ce mécanisme porte le doux nom de closures : en Javascript, une fonction définie dans une autre fonction peut accéder au scope de sa fonction parente, même si l'exécution de cette dernière est terminée.

Nous pourrions aller encore plus loin. En effet, la fonction makeUserClass n'est pas destinée à être réutilisée puisqu'elle est n'est appelée qu'une fois juste après sa définition. Pourquoi alourdir le code pour lui donner un nom ?

var User = (function() {
    var PASSWORD = 'gloubiboulga';

    var User = function(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.isAuthenticated = false;
    };

    User.prototype.toString = function() {
        return this.firstName + ' ' + this.lastName;
    };

    User.prototype.authenticate = function(password) {
        if (password == PASSWORD) {
            this.isAuthenticated = true;
        }
    };

    return User;
})();

Ici, notre fonction est définie puis appelée immédiatement grâce à la syntage (function() {…})(); On parle de IIFE, pour Immediately Invoked Function Expression.

La dernière étape sera de modifier encore un peu notre syntaxe pour faire de notre IIFE un véritable module, capable de définir et d'exporter plusiers objets à la fois.

(function(exports) {
    var PASSWORD = 'gloubiboulga';

    var User = function(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.isAuthenticated = false;
    };
    exports.User = User;

    User.prototype.toString = function() {
        return this.firstName + ' ' + this.lastName;
    };

    User.prototype.authenticate = function(password) {
        if (password == PASSWORD) {
            this.isAuthenticated = true;
        }
    };
})(this);

Dans le contexte global, this prend la valeur de l'objet global. En passant this en paramètre à notre module, on lui permet donc d'injecter dans le scope global tous les objets qu'il souhaite.

C'est beau comme du Beethoven, n'est-ce pas ?

Le mot de la fin

Voilà beaucoup de nouveaux concepts à digérer d'un coup, je vais donc m'arrêter là pour aujourd'hui. L'important à retenir est que Javascript est un langage qui a ses spécificités et qui ne se laissera pas apprivoiser à moins de prendre le temps de bien assimiler son fonctionnement et sa philosophie.

Au lieu de tenter bêtement de tordre Javascript pour y appliquer des concepts pour lesquels il n'est pas pertinent, pourquoi ne profiter de l'opportunité pour se débarrasser de quelques idées préconçues et embrasser de nouvelles façons de penser ?

Sur ce, à plus tard dans l'autocar.