2010-09-01 12 views
20

Creo que he entendido mal cómo funciona la herencia prototípica de Javascript. Específicamente, las variables internas de los prototipos parecen estar compartidas entre múltiples subobjetos diferentes. Es más fácil de ilustrar con código:Variables privadas en prototipos heredados

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());​ 

Esto da salida a 1 - 2 (prueba en JSBin), mientras que estaba totalmente convencida que el resultado sea 1 - 1, ya que quería dos objetos separados.

¿Cómo puedo asegurarme de que el objeto A no sea objeto compartido entre varias instancias de B?

actualización: This article destaca algunos de los problemas:

El problema es que el alcance de cada enfoque utiliza para crear una variable privada, que funciona bien, es también el cierre, en la acción, que los resultados en si cambia una variable privada para una instancia de objeto, se cambia para todos. Es decir. se parece más a una propiedad estática privada que a una variable privada real.

Por lo tanto, si desea tener algo privado, más como una constante no pública, cualquiera de los enfoques anteriores es bueno, pero no para las variables privadas reales. Las variables privadas solo funcionan muy bien con objetos singleton en JavaScript.

Solución: De acuerdo con la respuesta de BGerrissen, el cambio de la declaración de B y dejando del prototipo funciona como se pretende:

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

Respuesta

17

Los miembros privados son complicados con la herencia prototípica. Por un lado, no pueden ser heredados. Necesita crear miembros privados en cada constructor individual. Puedes hacer esto aplicando el super constructor en la subclase o creando un decorador.

decorador ejemplo:

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 

esto es sólo una variación de la llamada súper constructor, también puede lograr los mismos mediante una llamada al constructor de la superclase real con .apply()

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

Ahora mediante la aplicación de la herencia a través de B.prototype = new A() se invoca un código de constructor innecesario desde A.Una forma de evitar esto es utilizar Douglas Crockfords engendran método:

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

cual se utiliza de la siguiente manera:

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

Por supuesto, usted puede abandonar la herencia por completo y hacer buen uso de decoradores, al menos donde se necesitan miembros privados

5

El punto del prototipo es que se comparte entre varios objetos (es decir, aquellos que son creados por la misma función de constructor). Si necesita variables que no se comparten entre objetos que comparten un prototipo, debe mantener esas variables dentro de la función de constructor para cada objeto. Solo usa el prototipo para compartir métodos.

En su ejemplo, no puede hacer exactamente lo que quiere con prototipos. Esto es lo que podrías hacer en su lugar. Vea la respuesta de Daniel Earwicker para obtener más explicaciones de que no tiene sentido que yo me esté replicando aquí.

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

Pero el punto aquí es que la funcionalidad pertenece en 'A', y se comparte entre varios "clases" sub-('B', 'C' y 'D'). ¿No hay posibilidad de variables privadas no estáticas en superclases en Javascript? –

+1

Una forma común de significar intenciones 'privadas' (es decir, advirtiendo a los programadores que algo especial está sucediendo con una propiedad) es usar el guión bajo como prefijo del nombre de la propiedad (es decir, obj._privateMember). Aunque esto funcionará con la herencia de cadenas y números, comienza a volverse más complejo cuando se usan objetos y matrices como propiedades. – BGerrissen

+0

No tengo claro exactamente lo que estás preguntando. ¿Desea que una variable definida dentro de la función 'A' esté disponible para los métodos de 'B',' C' y 'D' pero en ninguna otra parte? Trate de no pensar en cosas en JavaScript en términos de características de otros lenguajes (como Java o C#). JavaScript simplemente no tiene clases. Tiene objetos que heredan propiedades de otros objetos a través de una cadena de prototipos. –

15

Tienes que olvidar la idea de las clases. Realmente no existe tal cosa en JavaScript como una 'instancia de B'. Solo hay 'algún objeto que hayas obtenido llamando a la función constructora B'. Un objeto tiene propiedades. Algunas son sus propiedades "propias", otras se incluyen al buscar en la cadena de prototipos.

Cuando dice new A, está creando un objeto. Luego lo asigna como el prototipo de B, lo que significa que cada llamada a new B produce un nuevo objeto que tiene el mismo prototipo directo y, por lo tanto, la misma variable de contador.

En la respuesta de Tim Down, se muestran dos alternativas. Su incrementPublic usa herencia, pero hace que la variable de contador sea pública (es decir, abandona la encapsulación). Mientras que incrementInternal hace que el contador sea privado pero tiene éxito moviendo el código a B (es decir, renuncia a la herencia).

Lo que queremos es una combinación de tres cosas:

  • comportamiento hereditario - por lo que debe ser definida en A y no requieren código en B, aparte de establecer el prototipo
  • datos privados, almacenado en el cierre -variables locales
  • datos por instancia, almacenados en this.

El problema es la contradicción entre los dos últimos. También diría que la herencia tiene un valor limitado en JS de todos modos. Es mejor tratarlo como un lenguaje funcional:

// 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() 
}; 

Personalmente, tienden a evitar las funciones constructoras y la herencia de prototipo mayor parte del tiempo. Son mucho menos útiles en JS de lo que suponen las personas de un entorno OO.

actualización me gustaría tener cuidado de la declaración que usted ha citado en su actualización

las variables privadas sólo funcionan muy bien con objetos únicos en JavaScript.

Eso no es verdad. Un objeto es solo un 'diccionario' de propiedades, cualquiera de las cuales puede ser funciones. Así que olvídate de los objetos y piensa en las funciones. Puede crear varias instancias de una función de acuerdo con un patrón, escribiendo una función que devuelve una función. El ejemplo de makeCounter es solo un simple ejemplo de esto. makeCounter no es un "objeto singleton" y no tiene que estar limitado al uso en objetos singleton.

+1

+1. Buena explicación. Dejaré mi respuesta tal como está y no intentaré reescribir lo que has escrito aquí. Personalmente, encuentro prototipos y constructores útiles, aunque solo sea para compartir métodos entre múltiples objetos relacionados. –

+0

+1 para aclarar la naturaleza prototípica. – BGerrissen

0

Acabo de encontrar otra solución difícil, sin exportar ningún método/variables a objetos públicos.

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)); 

Más de prueba y las muestras aquí: http://jsfiddle.net/oceog/TJH9Q/

Cuestiones relacionadas