L'opérateur new est-il une mauvaise pratique ?

On sait que l'opérateur new a été introduit en JavaScript pour rendre la programmation objet plus intuitive pour les développeurs accoutumés à la programmation objet orientée classes. La syntaxe est effectivement plus facile à prendre en main, mais présente aussi plusieurs inconvénients.

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
function Car(model){
    this.model = model;
   this.speed = 0;
}
 
Car.prototype.wheels = 4;
Car.prototype.drive = function(){
    this.speed = 120;
}
 
var car = new Car("Ferrari");
car.drive();
console.log(car.speed); // 120

L'obligation d'avoir une fonction constructeur

En POO par classes, un constructeur est nécessaire pour passer du modèle à l'objet réel, de la classe à l'instance. En POO par prototypes, le modèle est un objet réel. Une fonction constructeur n'est donc pas obligatoire. C'est un petit plus qui nous sert à passer les propriétés initiales de l'objet en inline, et appeler éventuellement d'autres fonctions ou déclencher des évènements à la création de l'objet. Avec new, nous sommes forcés d'avoir un constructeur. Et ce que le développeur assimile à la classe est en fait la fonction constructeur, ce qui est très perturbant.

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
car.constructor === Car; // true
 
car instanceof Car; //true : si car est une instance de Car, alors Car est ma classe ? Non ! C'est le constructeur ! >_>
Et si je souhaite ajouter le constructeur de la voiture dans mon modèle Car ?

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
function Car(constructor, model){
   this.model = model;
   this.constructor = constructor;
   this.speed = 0;
}
 
var golf = new Car("Volkswagen","Golf");
 
golf.constructor === Car; // false !

L'héritage devient sans raison bien plus compliqué

C'est là que la plupart des développeurs Java sont complètement largués. En suivant l'approche new, un héritage se ferait de la sorte :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
 
function Berline(constructor, model){
    Car.apply(this, arguments); //équivalent de super();
}
 
Berline.prototype = new Car(); // le modèle de la classe fils est une instance de la classe parente ???
 
var volvo = new Berline("Volvo","S60");
Pour expliquer cela, il faut se rappeler que les prototypes sont des objets "réels", instanciés. Seulement, Car n'est pas le prototype, mais le constructeur du prototype ! Il ne faut pas confondre non plus l'instance et son constructeur.

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
Object.getPrototypeOf(volvo); // Car {model: undefined, constructor: undefined, speed: 0}
Object.getPrototypeOf(volvo) === Car //false
Object.getPrototypeOf(Berline); // function Empty() {}
Tout cela est vraiment perturbant, et source de fréquentes erreurs. Nous sommes contraints de garder une référence aux constructeurs, mais ceux-ci n'ont aucune utilité à part leur usage avec new.


Le contexte this fonctionne différemment

Avec new, this ne fonctionne plus de la même façon dans la fonction constructeur. Il ne fait pas référence au scope parent comme d'habitude, mais à l'instance nouvellement créée. C'est un comportement tout à fait spécial et là encore très perturbant. Cela a aussi comme grave conséquence de modifier le contexte parent par erreur si on oublie l'opérateur new à l'instanciation :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
 
var toyota = Car("Toyota","Yaris"); //oups
console.log(window.model); // Yaris ! GLOBAL LEAK
console.log(window.constructor); // Toyota ! Aïe, j'ai même écrasé Window ! Bonjour les dégâts !

L'alternative : Object.create (support IE9+)

Voici un code similaire sans utiliser l'opérateur new :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
/* le prototype de base de tout objet est Object.prototype
   le prototype Car est un objet, on le crée via Object.create et on lui donne les propriétés qu'auront toutes les voitures */
var Car = Object.create(Object.prototype);
Car.wheels = 4;
Car.drive = function(){
   this.speed = 120;
};
 
// on crée une voiture à partir du prototype Car
var golf = Object.create(Car);
golf.constructor = "Volkswagen";
golf.model = "Golf";
 
Object.getPrototypeOf(golf) === Car; // true ! ENFIN
 
golf instanceof Car // TypeError: Expecting a function in instanceof check, but got #<Object> ; l'opérateur instanceof est à jeter avec new
Si on veut une fonction constructeur, on la déclare :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
 
Car.create = function(constructor, model ){
     return Object.create(Car, {
         constructor: { writable:true, configurable:true, value: constructor },
         model: { writable:true, configurable:true, value: model},
      });
};
 
var volvo = Car.create("Volvo","S60");
Notez les attributs writable, configurable... Il y en a d'autres, répertoriés ici : https://developer.mozilla.org/fr/doc...defineProperty
On peut paramétrer très précisément le comportement de la variable, et se rapprocher de la définition de variables privées ou semi-privées en surchargeant les getters et setters.

Le concept de l'héritage s'assimile très bien à celui de prototype :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
 
var Berline = Object.create(Car);
var peugeot = Object.create(Berline);
console.log(peugeot.wheels); //4
Résumé des avantages :
  • libre d'utiliser une fonction constructeur ou d'attribuer les propriétés manuellement ;
  • plus d'utilisation hasardeuse du mot-clé this ;
  • on garde une référence au prototype et non plus au constructeur ;
  • l'héritage est aussi simple qu'il devrait l'être.


Pour ceux qui ne peuvent pas se passer de constructeurs et qui souhaitent éviter d'en définir un pour chaque objet, Douglas Crockford a travaillé sur une fonction qui remplace efficacement l'opérateur new sans perdre les avantages précités. La voici :



Douglas CrockFord définit son propre "new", septembre 2011
https://www.youtube.com/watch?v=ya4UHuXNygM#t=3030



Et vous ?

Qu'en pensez-vous ? Doit-on bannir new et instanceof ?