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 |
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 |
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 ! >_> |
Code : | Sélectionner tout |
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 |
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"); |
Code : | Sélectionner tout |
1 2 3 4 | Object.getPrototypeOf(volvo); // Car {model: undefined, constructor: undefined, speed: 0} Object.getPrototypeOf(volvo) === Car //false Object.getPrototypeOf(Berline); // function Empty() {} |
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 |
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 |
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 |
Code : | Sélectionner tout |
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"); |
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 |
1 2 3 4 | var Berline = Object.create(Car); var peugeot = Object.create(Berline); console.log(peugeot.wheels); //4 |
- 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
Et vous ?
Qu'en pensez-vous ? Doit-on bannir new et instanceof ?