web-dev-qa-db-fra.com

Classes JavaScript

Je comprends les pseudo-classes JavaScript de base:

function Foo(bar) {
    this._bar = bar;
}

Foo.prototype.getBar = function() {
    return this._bar;
};

var foo = new Foo('bar');
alert(foo.getBar()); // 'bar'
alert(foo._bar); // 'bar'

Je comprends également le modèle de module, qui peut émuler l’encapsulation:

var Foo = (function() {
    var _bar;

    return {
        getBar: function() {
            return _bar;
        },
        setBar: function(bar) {
            _bar = bar;
        }
    };
})();

Foo.setBar('bar');
alert(Foo.getBar()); // 'bar'
alert(Foo._bar); // undefined

Mais il existe des propriétés semblables à un-OOP pour ces deux modèles. Le premier ne fournit pas d'encapsulation. Ce dernier ne fournit pas d'instanciation. Les deux modèles peuvent être modifiés pour prendre en charge les pseudo-héritages.

Ce que j'aimerais savoir, c'est s'il existe un modèle qui permet:

  • Héritage
  • Encapsulation (support des propriétés/méthodes "privées")
  • Instanciation (peut avoir plusieurs instances de la "classe", chacune avec son propre état)
45
FtDRbwLXw6

et ça : 

var Foo = (function() {
    // "private" variables 
    var _bar;

    // constructor
    function Foo() {};

    // add the methods to the prototype so that all of the 
    // Foo instances can access the private static
    Foo.prototype.getBar = function() {
        return _bar;
    };
    Foo.prototype.setBar = function(bar) {
        _bar = bar;
    };

    return Foo;
})();

Et maintenant nous avons l'instanciation, l'encapsulation et l'héritage.
Mais, il y a toujours un problème. La variable private est static car elle est partagée par toutes les instances de Foo. Démo rapide: 

var a = new Foo();
var b = new Foo();
a.setBar('a');
b.setBar('b');
alert(a.getBar()); // alerts 'b' :(    

Une meilleure approche pourrait être d’utiliser des conventions pour les variables privées: toute variable privée doit commencer par un trait de soulignement. Cette convention est bien connue et largement utilisée. Ainsi, lorsqu'un autre programmeur utilise ou modifie votre code et voit une variable commençant par un trait de soulignement, il saura qu'il est privé, pour un usage interne uniquement, et qu'il ne le modifiera pas.
Voici la réécriture utilisant cette convention: 

var Foo = (function() {
    // constructor
    function Foo() {
        this._bar = "some value";
    };

    // add the methods to the prototype so that all of the 
    // Foo instances can access the private static
    Foo.prototype.getBar = function() {
        return this._bar;
    };
    Foo.prototype.setBar = function(bar) {
        this._bar = bar;
    };

    return Foo;
})();

Nous avons maintenant une instanciation, un héritage, mais nous avons perdu notre encapsulation au profit de conventions: 

var a = new Foo();
var b = new Foo();
a.setBar('a');
b.setBar('b');
alert(a.getBar()); // alerts 'a' :) 
alert(b.getBar()); // alerts 'b' :) 

mais les vars privés sont accessibles: 

delete a._bar;
b._bar = null;
alert(a.getBar()); // alerts undefined :(
alert(b.getBar()); // alerts null :(
72
gion_13
7
Joe Davis

Les fermetures sont votre ami!

Ajoutez simplement la petite fonction suivante à votre espace de noms de premier niveau et vous êtes prêt pour la POO, avec

  • encapsulation, avec variables statiques et d'instance, privées et publiques et méthodes 
  • héritage
  • injection au niveau de la classe (par exemple, pour les services singleton)
  • pas de contraintes, pas de framework, juste du vieux Javascript

function clazz(_class, _super) {
    var _prototype = Object.create((_super || function() {}).prototype);
    var _deps = Array.isArray(_class) ? _class : [_class]; _class = _deps.pop();
    _deps.Push(_super);
    _prototype.constructor = _class.apply(_prototype, _deps) || _prototype.constructor;
    _prototype.constructor.prototype = _prototype;
    return _prototype.constructor;
}

La fonction ci-dessus relie simplement le prototype et le constructeur parent éventuel de la classe donnée, et renvoie le constructeur résultant, prêt pour l'instanciation.

Maintenant, vous pouvez plus naturellement déclarer vos classes de base (c'est-à-dire qui s'étendent {}) dans quelques lignes de code, complétées par des propriétés et des méthodes static, instance, public et private:

MyBaseClass = clazz(function(_super) { // class closure, 'this' is the prototype
    // local variables and functions declared here are private static variables and methods
    // properties of 'this' declared here are public static variables and methods
    return function MyBaseClass(arg1, ...) { // or: this.constructor = function(arg1, ...) {
        // local variables and functions declared here are private instance variables and methods
        // properties of 'this' declared here are public instance variables and methods
    };
});

Étendre une classe? C'est d'autant plus naturel:

MySubClass = clazz(function(_super) { // class closure, 'this' is the prototype
    // local variables and functions are private static variables and methods
    // properties of this are public static variables and methods
    return function MySubClass(arg1, ...) // or: this.constructor = function(arg1, ...) {
        // local variables and functions are private instance variables and methods
        _super.apply(this, arguments); // or _super.call(this, arg1, ...)
        // properties of 'this' are public instance variables and methods
    };
}, MyBaseClass); // extend MyBaseClass

En d'autres termes, passez le constructeur de la classe parent à la fonction clazz et ajoutez _super.call(this, arg1, ...) au constructeur de la classe enfant, qui appelle le constructeur de la classe parent avec les arguments requis. Comme pour tout schéma d'héritage standard, l'appel du constructeur parent doit venir en premier dans le constructeur enfant.

Notez que vous êtes libre de nommer explicitement le constructeur avec this.constructor = function(arg1, ...) {...} ou this.constructor = function MyBaseClass(arg1, ...) {...} si vous avez besoin d'un accès simple au constructeur à partir du code contenu dans le constructeur, ou même de simplement renvoyer le constructeur avec return function MyBaseClass(arg1, ...) {...} comme dans le code ci-dessus. Celui avec lequel vous vous sentez le plus à l'aise.

Installez simplement des objets à partir de classes telles que vous le feriez normalement avec un constructeur: myObj = new MyBaseClass();

Notez que les fermetures encapsulent bien toutes les fonctionnalités d'une classe, y compris son prototype et son constructeur, en fournissant un espace de noms naturel pour les méthodes et propriétés statiques et d'instance, privées et publiques. Le code dans une fermeture de classe est totalement libre de contraintes. Pas de framework, pas de contraintes, juste du vieux Javascript. Les fermetures règnent!

Oh, et si vous voulez injecter des dépendances singleton (par exemple, des services) dans votre classe (par exemple, un prototype), clazz le fera pour vous à la AngularJS:

DependentClass = clazz([aService, function(_service, _super) { // class closure, 'this' is the prototype
    // the injected _service dependency is available anywhere in this class
    return function MySubClass(arg1, ...) // or: this.constructor = function(arg1, ...) {
        _super.apply(this, arguments); // or _super.call(this, arg1, ...)
        // the injected _service dependency is also available in the constructor
    };
}], MyBaseClass); // extend MyBaseClass

Comme le code ci-dessus tente de l'illustrer, pour injecter des singletons dans une classe, placez simplement la fermeture de la classe en tant que dernière entrée dans un tableau avec toutes ses dépendances. Ajoutez également les paramètres correspondants à la fermeture de classe devant le paramètre _super et dans le même ordre que dans le tableau. clazz injectera les dépendances du tableau sous forme d'arguments dans la fermeture de la classe. Les dépendances sont alors disponibles n'importe où dans la fermeture de classe, y compris le constructeur.

En fait, puisque les dépendances sont injectées dans le prototype, elles sont disponibles pour les méthodes statiques avant même l’instanciation d’un objet à partir de la classe. Ceci est très puissant pour le câblage de vos applications ou de vos tests unitaires et de bout en bout. Cela supprime également la nécessité d'injecter des singletons dans les constructeurs, qui sinon entraveraient inutilement le code du constructeur.

Vérifiez ce violon: http://jsfiddle.net/5uzmyvdq/1/

Vos commentaires et suggestions sont les bienvenus!

4
Stephane Catala

Les classes JavaScript sont introduites dans ECMAScript 6 et constituent un sucre syntaxique par rapport à l'héritage existant de JavaScript basé sur un prototype. La syntaxe de la classe n'introduit pas un nouveau modèle d'héritage orienté objet dans JavaScript. Les classes JavaScript fournissent une syntaxe beaucoup plus simple et claire pour créer des objets et gérer l'héritage.

Vous pouvez en voir plus dans ce lien Communauté Mozilla

Github

1
user1938455

Un problème avec beaucoup de classes JS, c'est qu'elles ne sécurisent pas leurs champs et leurs méthodes, ce qui signifie que toute personne qui les utilise peut remplacer accidentellement une méthode. Par exemple le code:

function Class(){
    var name="Luis";
    var lName="Potter";
}

Class.prototype.changeName=function(){
    this.name="BOSS";
    console.log(this.name);
};

var test= new Class();
console.log(test.name);
test.name="ugly";
console.log(test.name);
test.changeName();
test.changeName=function(){
    console.log("replaced");
};
test.changeName();
test.changeName();

affichera:

ugly
BOSS
replaced 
replaced 

Comme vous pouvez le voir, la fonction changeName est remplacée. Le code suivant sécuriserait les méthodes et les champs de la classe et les accesseurs/assembleurs seraient utilisés pour y accéder, ce qui en ferait une classe "normale" trouvée dans d'autres langues.

function Class(){
    var name="Luis";
    var lName="Potter";

    function getName(){
         console.log("called getter"); 
         return name;
    };

    function setName(val){
         console.log("called setter"); 
         name = val
    };

    function getLName(){
         return lName
    };

    function setLName(val){
        lName = val;
    };

    Object.defineProperties(this,{
        name:{
            get:getName, 
            set:setName, 
            enumerable:true, 
            configurable:false
        },
        lastName:{
            get:getLName, 
            set:setLName, 
            enumerable:true, 
            configurable:false
        }
    });
}

Class.prototype.changeName=function(){
    this.name="BOSS";
};   

Object.defineProperty(Class.prototype, "changeName", {
    writable:false, 
    configurable:false
});

var test= new Class();
console.log(test.name);
test.name="ugly";
console.log(test.name);
test.changeName();
test.changeName=function(){
    console.log("replaced")
};
test.changeName();
test.changeName();

Cela génère:

called getter
Luis
called setter 
called getter 
ugly 
called setter 
called setter 
called setter 

Désormais, vos méthodes de classe ne peuvent pas être remplacées par des valeurs ou des fonctions aléatoires et le code des getters et des setters est toujours exécuté lors d'une tentative de lecture ou d'écriture dans le champ.

0
Piacenti

Je pensais à ce sujet particulier récemment et aux limites des différentes approches. La meilleure solution que j'ai pu trouver est celle ci-dessous. 

Il semble résoudre les problèmes d’héritage, d’instanciation et d’écapsulation (du moins à partir des tests effectués sur Google Chrome v.24) bien qu’il ait probablement un coût d’utilisation de la mémoire.

function ParentClass(instanceProperty) {
  // private
  var _super = Object.create(null),
      privateProperty = "private " + instanceProperty;
  // public
  var api = Object.create(_super);
  api.constructor = this.constructor;
  api.publicMethod = function() {
     console.log( "publicMethod on ParentClass" );
     console.log( privateProperty );
  };
  api.publicMethod2 = function() {
     console.log( "publicMethod2 on ParentClass" );
     console.log( privateProperty );
  };
  return api;
}

function SubClass(instanceProperty) {
    // private
    var _super = ParentClass.call( this, instanceProperty ),
        privateProperty = "private sub " + instanceProperty;
    // public
    var api = Object.create(_super);
    api.constructor = this.constructor;
    api.publicMethod = function() {
       _super.publicMethod.call(this); // call method on ParentClass
        console.log( "publicMethod on SubClass" );
        console.log( privateProperty );
    }
    return api;
}

var par1 = new ParentClass(0),
    par2 = new ParentClass(1),
    sub1 = new SubClass(2),
    sub2 = new SubClass(3);

par1.publicMethod();
par2.publicMethod();
sub1.publicMethod();
sub2.publicMethod();
par1.publicMethod2();
par2.publicMethod2();
sub1.publicMethod2();
sub2.publicMethod2();
0
BrettJephson