2010-09-01 11 views
20

Je pense avoir mal compris comment fonctionne l'héritage prototypique de Javascript. Plus précisément, les variables internes des prototypes semblent être partagées entre plusieurs sous-objets différents. Il est plus facile d'illustrer avec le code:Variables privées dans les prototypes hérités

var A = function() 
{ 
    var internal = 0; 
    this.increment = function() 
    { 
    return ++internal; 
    }; 
}; 

var B = function() {}; 
// inherit from A 
B.prototype = new A; 

x = new B; 
y = new B; 

$('#hello').text(x.increment() + " - " + y.increment());​ 

Ce sorties 1 - 2 (test sur JSBin), alors que j'attendais que le résultat soit 1 - 1, puisque je voulais deux objets séparés.

Comment puis-je m'assurer que l'objet A n'est pas un objet partagé entre plusieurs instances de B?

Mise à jour: This article met en évidence certaines des questions:

Le problème est que la portée de chaque approche utilise pour créer une variable privée, qui fonctionne très bien, est aussi la fermeture, dans l'action, que les résultats Si vous modifiez une variable privée pour une instance d'objet, elle est modifiée pour tous. C'est à dire. c'est plus comme une propriété statique privée qu'une variable privée réelle. Donc, si vous voulez avoir quelque chose de privé, plus comme une constante non publique, l'une des approches ci-dessus est bonne, mais pas pour les variables privées réelles. Les variables privées ne fonctionnent que très bien avec les objets singleton en JavaScript.

Solution: Selon la réponse de BGerrissen, changer la déclaration de B et partant du prototype fonctionne comme prévu:

var B = function() { A.apply(this, arguments); }; 

Répondre

17

Les membres privés sont difficiles en utilisant l'héritage prototypique. Pour un, ils ne peuvent pas être hérités. Vous devez créer des membres privés dans chaque constructeur individuel. Vous pouvez le faire soit en appliquant le super constructeur dans la sous-classe, soit en créant un décorateur.

exemple Décorateur:

function internalDecorator(obj){ 
    var internal = 0; 
    obj.increment = function(){ 
     return ++internal; 
    } 
} 

var A = function(){ 
    internalDecorator(this); 
} 
A.prototype = {public:function(){/*etc*/}} 

var B = function(){ 
    internalDecorator(this); 
} 
B.prototype = new A(); // inherits 'public' but ALSO redundant private member code. 

var a = new B(); // has it's own private members 
var b = new B(); // has it's own private members 

Ceci est juste une variante du super appel du constructeur, vous pouvez également réaliser la même en appelant le constructeur super réelle avec .apply()

var B = function(){ 
    A.apply(this, arguments); 
} 

Maintenant, en appliquant l'héritage par B.prototype = new A() vous invoquez le code constructeur inutile de A.Une façon d'éviter cela est d'utiliser Douglas Crockfords engendreras méthode:

Object.beget = function(obj){ 
    var fn = function(){} 
    fn.prototype = obj; 
    return new fn(); // now only its prototype is cloned. 
} 

que vous utilisez comme suit:

B.prototype = Object.beget(A.prototype); 

Bien sûr, vous pouvez abandonner l'héritage tout à fait et faire bon usage de décorateurs, au moins où les membres privés sont nécessaires.

5

Le point du prototype est qu'il est partagé entre plusieurs objets (c'est-à-dire ceux qui sont créés par la même fonction de constructeur). Si vous avez besoin de variables qui ne sont pas partagées entre des objets partageant un prototype, vous devez conserver ces variables dans la fonction constructeur pour chaque objet. Utilisez simplement le prototype pour partager les méthodes.

Dans votre exemple, vous ne pouvez pas faire exactement ce que vous voulez en utilisant des prototypes. Voici ce que vous pourriez faire à la place. Voir la réponse de Daniel Earwicker pour plus d'explications qu'il n'y a pas lieu de répliquer ici.

var A = function() {}; 

A.prototype.incrementPublic = function() 
{ 
    return ++this.publicProperty; 
}; 

var B = function() 
{ 
    this.publicProperty = 0; 
    var internal = 0; 
    this.incrementInternal = function() 
    { 
     return ++internal; 
    }; 
}; 

B.prototype = new A(); 

var x = new B(), y = new B(); 
console.log(x.incrementPublic(), y.incrementPublic()); // 1, 1 
console.log(x.incrementInternal(), y.incrementInternal()); // 1, 1 
+1

Mais le point ensemble est ici que la fonctionnalité appartient à 'A', et est partagé entre plusieurs sous "classes"(' B', 'C' et 'D'). N'y a-t-il pas de possibilité pour les variables privées non statiques dans les superclasses en Javascript? –

+1

Une façon courante de signifier des intentions «privées» (comme avertir les programmeurs que quelque chose de spécial se passe avec une propriété) consiste à utiliser le trait de soulignement comme préfixe du nom de la propriété (par exemple, obj._privateMember). Bien que cela fonctionne avec l'héritage pour les chaînes et les nombres, il devient plus complexe lorsque vous utilisez des objets et des tableaux en tant que propriétés. – BGerrissen

+0

Je ne comprends pas exactement ce que vous demandez. Voulez-vous qu'une variable définie dans la fonction 'A' soit disponible pour les méthodes de' B', 'C' et' D' mais nulle part ailleurs? Essayez de ne pas penser aux choses en JavaScript en termes de fonctionnalités d'autres langues (telles que Java ou C#). JavaScript n'a tout simplement pas de classes. Il a des objets qui héritent des propriétés d'autres objets via une chaîne prototype. –

15

Vous devez oublier l'idée des cours. Il n'y a pas vraiment une telle chose dans JavaScript comme une «instance de B». Il y a seulement 'un objet que vous avez obtenu en appelant la fonction de constructeur B'. Un objet a des propriétés. Certaines sont ses propres propriétés, d'autres sont incluses en cherchant dans la chaîne du prototype.

Lorsque vous dites new A, vous créez un objet. Ensuite, vous l'attribuez comme prototype pour B, ce qui signifie que chaque appel à new B produit un nouvel objet qui a le même prototype direct, et donc la même variable de compteur.

Dans la réponse de Tim Down, deux alternatives sont affichées. Son incrementPublic utilise l'héritage, mais rend la variable de compteur publique (c'est-à-dire abandonne l'encapsulation). Alors que incrementInternal rend le compteur privé mais réussit en déplaçant le code dans B (c.-à-d. Abandonne l'héritage).

Ce que vous voulez est une combinaison de trois choses:

  • comportement héritable - il doit être défini dans A et ne nécessitent pas de code B en dehors de réglage du prototype
  • données privées, stockées dans la fermeture -les variables locales
  • données par instance, stockées dans this.

Le problème est la contradiction entre les deux derniers. Je dirais aussi que l'héritage a une valeur limitée dans JS de toute façon. Il est préférable de le traiter comme un langage fonctionnel:

// higher-order function, returns another function with counter state 
var makeCounter = function() { 
    var c = 0; 
    return function() { return ++c; }; 
}; 

// make an object with an 'increment' method: 
var incrementable = { 
    increment: makeCounter() 
}; 

Personnellement, je tendance à éviter les fonctions constructeur et l'héritage de prototype la plupart du temps. Ils sont beaucoup moins utiles dans JS que les gens d'un contexte OO supposer.

Mise à jour je serais méfiant de la déclaration que vous avez cité dans votre mise à jour:

Les variables privées ne vraiment bien travailler avec des objets singleton dans JavaScript.

Ce n'est pas vraiment vrai. Un objet est juste un «dictionnaire» de propriétés, toutes pouvant être des fonctions. Donc, oubliez les objets et pensez aux fonctions. Vous pouvez créer plusieurs instances d'une fonction en fonction d'un modèle, en écrivant une fonction qui renvoie une fonction. L'exemple makeCounter n'est qu'un simple exemple de ceci. makeCounter n'est pas un "objet singleton" et ne doit pas nécessairement être utilisé dans des objets singleton.

+1

+1. Bonne explication. Je vais laisser ma réponse telle quelle et ne pas essayer de réécrire ce que vous avez écrit ici. Personnellement, je trouve les prototypes et les constructeurs utiles, ne serait-ce que pour partager des méthodes entre plusieurs objets connexes. –

+0

+1 pour une nature prototypique éclairante. – BGerrissen

0

Je viens de trouver d'autres solutions délicates, sans exporter de méthodes/variables vers des objets publics.

function A(inherit) { 
    var privates = { //setup private vars, they could be also changed, added in method or children 
     a: 1, 
     b: 2, 
     c: 3 
    }; 
    //setup public methods which uses privates 
    this.aPlus = bindPlus("aPlus", this, privates); //pass method name as string! 
    this.aGet = bindPlus("aGet", this, privates); 
    if (inherit) { 
     return privates; 
    } 
} 
A.prototype.aPlus = function() { 
    var args = getArgs(arguments), 
     //self is "this" here 
     self = args.shift(), 
     privates = args.shift(), 
     //function real arguments 
     n = args.shift(); 
    return privates.a += n; 
}; 

A.prototype.aGet = function (n) { 
    var args = getArgs(arguments), 
     self = args.shift(), 
     privates = args.shift(); 
    console.log(this, self, privates); 
    return privates.a; 
}; 

//utilites 
function getArgs(arg) { 
    return Array.prototype.slice.call(arg); 
} 

function bindPlus(funct, self, privates) { 
    return function() { 
     return Object.getPrototypeOf(self)[funct].bind(this, self, privates).apply(null, arguments); 
    }; 
} 

//inherited 
function B(inherit) { 
    var privates = Object.getPrototypeOf(this).constructor.call(this, true); 
    privates.d = 4; 
    this.dGet = bindPlus("dGet", this, privates); 
    if (inherit) { 
     return privates; 
    } 
} 

B.prototype = Object.create(A.prototype); 
B.constructor = B; 

B.prototype.aGet = function() { 
    var args = getArgs(arguments), 
     self = args.shift(), 
     privates = args.shift(); 
    console.warn("B.aGet", this, privates); 
    return privates.a; 
}; 

B.prototype.dGet = function() { 
    var args = getArgs(arguments), 
     self = args.shift(), 
     privates = args.shift(); 
    console.warn("B.dGet", this, privates); 
    return privates.d; 
}; 


// tests 
var b = new B(); 
var a = new A(); 

//should be 223 
console.log("223 ?",b.aPlus(222)); 

//should be 42 
console.log("41",a.aPlus(222)); 

Plus d'essai et des échantillons ici: http://jsfiddle.net/oceog/TJH9Q/

Questions connexes