Une introduction à la programmation orientée objet en JavaScript: objets, prototypes et classes

Dans de nombreux langages de programmation, les classes sont un concept bien défini. En JavaScript, ce n'est pas le cas. Ou du moins ce n'était pas le cas. Si vous recherchez OOP et JavaScript, vous rencontrerez de nombreux articles avec de nombreuses recettes différentes sur la façon dont vous pouvez émuler un classJavaScript.

Existe-t-il un moyen simple et KISS de définir une classe en JavaScript? Et si oui, pourquoi tant de recettes différentes pour définir une classe?

Avant de répondre à ces questions, comprenons mieux ce qu'est un JavaScript Object.

Objets en JavaScript

Commençons par un exemple très simple:

const a = {}; a.foo = 'bar';

Dans l'extrait de code ci-dessus, un objet est créé et amélioré avec une propriété foo. La possibilité d'ajouter des éléments à un objet existant est ce qui différencie JavaScript des langages classiques comme Java.

Plus en détail, le fait qu'un objet puisse être amélioré permet de créer une instance d'une classe «implicite» sans avoir besoin de créer réellement la classe. Clarifions ce concept avec un exemple:

function distance(p1, p2) { return Math.sqrt( (p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2 ); } distance({x:1,y:1},{x:2,y:2});

Dans l'exemple ci-dessus, je n'avais pas besoin d'une classe Point pour créer un point, j'ai simplement étendu une instance d' Objectajout xet de ypropriétés. La distance de la fonction ne se soucie pas si les arguments sont une instance de la classe Pointou non. Tant que vous n'appelez pas distancefunction avec deux objets qui ont une propriété xet yde type Number, cela fonctionnera très bien. Ce concept est parfois appelé typage canard .

Jusqu'à présent, je n'ai utilisé qu'un objet de données: un objet contenant uniquement des données et aucune fonction. Mais en JavaScript, il est possible d'ajouter des fonctions à un objet:

const point1 = { x: 1, y: 1, toString() { return `(${this.x},${this.y})`; } }; const point2 = { x: 2, y: 2, toString() { return `(${this.x},${this.y})`; } };

Cette fois, les objets représentant un point 2D ont une toString()méthode. Dans l'exemple ci-dessus, le toStringcode a été dupliqué et ce n'est pas bon.

Il existe de nombreuses façons d'éviter cette duplication et, en fait, dans différents articles sur les objets et les classes dans JS, vous trouverez différentes solutions. Avez-vous déjà entendu parler du «modèle de module de révélation»? Il contient les mots «modèle» et «révélateur», sonne bien, et «module» est un must. Donc ça doit être la bonne façon de créer des objets… sauf que ce n'est pas le cas. La révélation d'un modèle de module peut être le bon choix dans certains cas, mais ce n'est certainement pas la manière par défaut de créer des objets avec des comportements.

Nous sommes maintenant prêts à introduire des classes.

Classes en JavaScript

Qu'est-ce qu'une classe? À partir d'un dictionnaire: une classe est «un ensemble ou une catégorie d'objets ayant une propriété ou un attribut en commun et différenciés des autres par leur nature, type ou qualité»

Dans les langages de programmation, nous disons souvent «Un objet est une instance d'une classe». Cela signifie qu'en utilisant une classe, je peux créer de nombreux objets et ils partagent tous des méthodes et des propriétés.

Comme les objets peuvent être améliorés, comme nous l'avons vu précédemment, il existe des moyens de créer des méthodes et des propriétés de partage d'objets. Mais nous voulons le plus simple.

Heureusement, ECMAScript 6 fournit le mot-clé class, ce qui facilite la création d'une classe:

class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x},${this.y})`; } }

Donc, à mon avis, c'est la meilleure façon de déclarer des classes en JavaScript. Les classes sont souvent liées à l'héritage:

class Point extends HasXY { constructor(x, y) { super(x, y); } toString() { return `(${this.x},${this.y})`; } }

Comme vous pouvez le voir dans l'exemple ci-dessus, pour étendre une autre classe, il suffit d'utiliser le mot-clé extends.

Vous pouvez créer un objet à partir d'une classe à l'aide de l' newopérateur:

const p = new Point(1,1); console.log(p instanceof Point); // prints true

Une bonne façon de définir des classes orientée objet devrait fournir:

  • une syntaxe simple pour déclarer une classe
  • un moyen simple d'accéder à l'instance actuelle, aka this
  • une syntaxe simple pour étendre une classe
  • un moyen simple d'accéder à l'instance de la super classe, aka super
  • éventuellement, un moyen simple de dire si un objet est une instance d'une classe particulière. obj instanceof AClassdevrait retourner truesi cet objet est une instance de cette classe.

La nouvelle classsyntaxe fournit tous les points ci-dessus.

Avant l'introduction du classmot - clé, comment définir une classe en JavaScript?

De plus, qu'est-ce qu'une classe en JavaScript? Pourquoi parle-t-on souvent de prototypes ?

Cours en JavaScript 5

Depuis la page Mozilla MDN sur les classes:

Les classes JavaScript, introduites dans ECMAScript 2015, sont principalement du sucre syntaxique par rapport à l'héritage basé sur un prototype existant de JavaScript . La syntaxe de classe n'introduit pas de nouveau modèle d'héritage orienté objet dans JavaScript.

Le concept clé ici est l' héritage basé sur un prototype . Puisqu'il y a beaucoup de malentendus sur ce qu'est ce type d'héritage, je vais procéder étape par étape, passant d'un classmot- functionclé à l'autre.

class Shape {} console.log(typeof Shape); // prints function

Il semble que classet functionsont liés. Est-ce classjuste un alias pour function? Non, ça ne l'est pas.

Shape(2); // Uncaught TypeError: Class constructor Shape cannot be invoked without 'new'

Ainsi, il semble que les personnes qui ont introduit le classmot clé voulaient nous dire qu'une classe est une fonction qui doit être appelée à l'aide de l' newopérateur.

var Shape = function Shape() {} // Or just function Shape(){} var aShape = new Shape(); console.log(aShape instanceof Shape); // prints true

The example above shows that we can use function to declare a class. We cannot, however, force the user to call the function using the new operator. It is possible to throw an exception if the new operator wasn’t used to call the function.

Anyway I suggest you don’t put that check in every function that acts as a class. Instead use this convention: any function whose name begins with a capital letter is a class and must be called using the new operator.

Let’s move on, and find out what a prototype is:

class Shape { getName() { return 'Shape'; } } console.log(Shape.prototype.getName); // prints function getName() ...

Each time you declare a method inside a class, you actually add that method to the prototype of the corresponding function. The equivalent in JS 5 is:

function Shape() {} Shape.prototype.getName = function getName() { return 'Shape'; }; console.log(new Shape().getName()); // prints Shape

Sometimes the class-functions are called constructors because they act like constructors in a regular class.

You may wonder what happens if you declare a static method:

class Point { static distance(p1, p2) { // ... } } console.log(Point.distance); // prints function distance console.log(Point.prototype.distance); // prints undefined

Since static methods are in a 1 to 1 relation with classes, the static function is added to the constructor-function, not to the prototype.

Let’s recap all these concepts in a simple example:

function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function toString() { return '(' + this.x + ',' + this.y + ')'; }; Point.distance = function distance() { // ... } console.log(new Point(1,2).toString()); // prints (1,2) console.log(new Point(1,2) instanceof Point); // prints true

Up to now, we have found a simple way to:

  • declare a function that acts as a class
  • access the class instance using the this keyword
  • create objects that are actually an instance of that class (new Point(1,2) instanceof Point returns true )

But what about inheritance? What about accessing the super class?

class Hello { constructor(greeting) { this._greeting = greeting; } greeting() { return this._greeting; } } class World extends Hello { constructor() { super('hello'); } worldGreeting() { return super.greeting() + ' world'; } } console.log(new World().greeting()); // Prints hello console.log(new World().worldGreeting()); // Prints hello world

Above is a simple example of inheritance using ECMAScript 6, below the same example using the the so called prototype inheritance:

function Hello(greeting) { this._greeting = greeting; } Hello.prototype.greeting = function () { return this._greeting; }; function World() { Hello.call(this, 'hello'); } // Copies the super prototype World.prototype = Object.create(Hello.prototype); // Makes constructor property reference the sub class World.prototype.constructor = World; World.prototype.worldGreeting = function () { const hello = Hello.prototype.greeting.call(this); return hello + ' world'; }; console.log(new World().greeting()); // Prints hello console.log(new World().worldGreeting()); // Prints hello world

This way of declaring classes is also suggested in the Mozilla MDN example here.

Using the class syntax, we deduced that creating classes involves altering the prototype of a function. But why is that so? To answer this question we must understand what the new operator actually does.

New operator in JavaScript

The new operator is explained quite well in the Mozilla MDN page here. But I can provide you with a relatively simple example that emulates what the new operator does:

function customNew(constructor, ...args) { const obj = Object.create(constructor.prototype); const result = constructor.call(obj, ...args); return result instanceof Object ? result : obj; } function Point() {} console.log(customNew(Point) instanceof Point); // prints true

Note that the real new algorithm is more complex. The purpose of the example above is just to explain what happens when you use the new operator.

When you write new Point(1,2)what happens is:

  • The Point prototype is used to create an object.
  • The function constructor is called and the just created object is passed as the context (a.k.a. this) along with the other arguments.
  • If the constructor returns an Object, then this object is the result of the new, otherwise the object created from the prototype is the result.

So, what does prototype inheritance mean? It means that you can create objects that inherit all the properties defined in the prototype of the function that was called with the new operator.

If you think of it, in a classical language the same process happens: when you create an instance of a class, that instance can use the this keyword to access to all the functions and properties (public) defined in the class (and the ancestors). As opposite to properties, all the instances of a class will likely share the same references to the class methods, because there is no need to duplicate the method’s binary code.

Functional programming

Sometimes people say that JavaScript is not well suited for Object Oriented programming, and you should use functional programming instead.

While I don’t agree that JS is not suited for O.O.P, I do think that functional programming is a very good way of programming. In JavaScript functions are first class citizens (e.g. you can pass a function to another function) and it provides features like bind , call or apply which are base constructs used in functional programming.

In addition RX programming could be seen as an evolution (or a specialization) of functional programming. Have a look to RxJs here.

Conclusion

Use, when possible, ECMAScript 6 class syntax:

class Point { toString() { //... } }

or use function prototypes to define classes in ECMAScript 5:

function Point() {} Point.prototype.toString = function toString() { // ... }

Hope you enjoyed the reading!