Acceso a variables miembro privadas desde funciones definidas por prototipo


187

¿Hay alguna forma de hacer que las variables "privadas" (las definidas en el constructor) estén disponibles para los métodos definidos por el prototipo?

TestClass = function(){
    var privateField = "hello";
    this.nonProtoHello = function(){alert(privateField)};
};
TestClass.prototype.prototypeHello = function(){alert(privateField)};

Esto funciona:

t.nonProtoHello()

Pero esto no:

t.prototypeHello()

Estoy acostumbrado a definir mis métodos dentro del constructor, pero me estoy alejando de eso por un par de razones.



14
@ecampver, excepto que este fue preguntado 2 años antes ...
Pacerier

Respuestas:


191

No, no hay forma de hacerlo. Eso sería esencialmente un alcance inverso.

Los métodos definidos dentro del constructor tienen acceso a variables privadas porque todas las funciones tienen acceso al alcance en el que se definieron.

Los métodos definidos en un prototipo no están definidos dentro del alcance del constructor y no tendrán acceso a las variables locales del constructor.

Todavía puede tener variables privadas, pero si desea que los métodos definidos en el prototipo tengan acceso a ellas, debe definir captadores y definidores en el thisobjeto, a los que tendrán acceso los métodos prototipo (junto con todo lo demás) . Por ejemplo:

function Person(name, secret) {
    // public
    this.name = name;

    // private
    var secret = secret;

    // public methods have access to private members
    this.setSecret = function(s) {
        secret = s;
    }

    this.getSecret = function() {
        return secret;
    }
}

// Must use getters/setters 
Person.prototype.spillSecret = function() { alert(this.getSecret()); };

14
"scoping in reverse" es una característica de C ++ con la palabra clave "amigo". Esencialmente, cualquier función debe definir su prototipo como amigo. Lamentablemente, este concepto es C ++ y no JS :(
TWiStErRob

1
Me gustaría agregar esta publicación a la parte superior de mi lista de favoritos y mantenerla allí.
Donato

2
No veo el punto de esto: solo está agregando una capa de abstracción que no hace nada. También puedes hacersecret una propiedad de this. JavaScript simplemente no admite variables privadas con prototipos, ya que los prototipos están vinculados al contexto del sitio de llamada, no al contexto del 'sitio de creación'.
nicodemus13

1
¿Por qué no simplemente hacer person.getSecret()entonces?
Fahmi

1
¿Por qué esto tiene tantos votos a favor? Esto no hace que la variable sea privada. Como se mencionó anteriormente, usar person.getSecret () le permitirá acceder a esa variable privada desde cualquier lugar.
alexr101

64

Actualización: con ES6, hay una mejor manera:

En pocas palabras, puede usar el nuevo Symbolpara crear campos privados.
Aquí hay una gran descripción: https://curiosity-driven.org/private-properties-in-javascript

Ejemplo:

var Person = (function() {
    // Only Person can access nameSymbol
    var nameSymbol = Symbol('name');

    function Person(name) {
        this[nameSymbol] = name;
    }

    Person.prototype.getName = function() {
        return this[nameSymbol];
    };

    return Person;
}());

Para todos los navegadores modernos con ES5:

Puedes usar solo cierres

La forma más sencilla de construir objetos es evitar por completo la herencia de prototipos. Simplemente defina las variables privadas y las funciones públicas dentro del cierre, y todos los métodos públicos tendrán acceso privado a las variables.

O puede usar solo prototipos

En JavaScript, la herencia prototípica es principalmente una optimización . Permite que varias instancias compartan métodos prototipo, en lugar de que cada instancia tenga sus propios métodos.
El inconveniente es que thises lo único diferente cada vez que se llama a una función prototípica.
Por lo tanto, cualquier campo privado debe ser accesible a través de this, lo que significa que serán públicos. Así que nos limitamos a las convenciones de nomenclatura para _privatecampos.

No te molestes en mezclar cierres con prototipos

Creo que no deberías mezclar variables de cierre con métodos prototipo. Deberías usar uno u otro.

Cuando utiliza un cierre para acceder a una variable privada, los métodos prototipo no pueden acceder a la variable. Entonces, tienes que exponer el cierrethis , lo que significa que lo está exponiendo públicamente de una forma u otra. Hay muy poco que ganar con este enfoque.

¿Cuál elijo?

Para objetos realmente simples, solo use un objeto simple con cierres.

Si necesita una herencia prototípica, por herencia, rendimiento, etc., entonces cumpla con la convención de nomenclatura "_private" y no se moleste con los cierres.

No entiendo por qué los desarrolladores de JS se esfuerzan TAN por hacer que los campos sean realmente privados.


44
Lamentablemente, la _privateconvención de nomenclatura sigue siendo la mejor solución si desea aprovechar la herencia de prototipos.
aplastar

1
ES6 tendrá un nuevo concepto, el Symbol, que es una excelente manera de crear campos privados. Aquí hay una gran explicación: curiosity-driven.org/private-properties-in-javascript
Scott Rippey

1
No, puede mantener el Symbolcierre en una clase que abarque toda su clase. De esa manera, todos los métodos prototipo pueden usar el Símbolo, pero nunca se expone fuera de la clase.
Scott Rippey

2
El artículo se ha vinculado dice " Los símbolos son similares a los nombres privados, pero - a diferencia de nombres privado - que no proporcionan la verdadera intimidad . ". Efectivamente, si tiene la instancia, puede obtener sus símbolos con Object.getOwnPropertySymbols. Entonces esto es solo privacidad por oscuridad.
Oriol

2
@ Oriol Sí, la privacidad es a través de una gran oscuridad. Todavía es posible iterar a través de los símbolos, e infiere el propósito del símbolo a través de toString. Esto no es diferente de Java o C # ... todavía se puede acceder a los miembros privados a través de la reflexión, pero generalmente están muy ocultos. Todo lo cual sirve para fortalecer mi punto final: "No entiendo por qué los desarrolladores de JS se esfuerzan TAN por hacer que los campos sean realmente privados".
Scott Rippey

31

Cuando leí esto, sonaba como un desafío difícil, así que decidí encontrar la manera. Lo que se me ocurrió fue CRAAAAZY pero funciona totalmente.

Primero, intenté definir la clase en una función inmediata para que tuviera acceso a algunas de las propiedades privadas de esa función. Esto funciona y le permite obtener algunos datos privados, sin embargo, si intenta establecer los datos privados, pronto encontrará que todos los objetos compartirán el mismo valor.

var SharedPrivateClass = (function() { // use immediate function
    // our private data
    var private = "Default";

    // create the constructor
    function SharedPrivateClass() {}

    // add to the prototype
    SharedPrivateClass.prototype.getPrivate = function() {
        // It has access to private vars from the immediate function!
        return private;
    };

    SharedPrivateClass.prototype.setPrivate = function(value) {
        private = value;
    };

    return SharedPrivateClass;
})();

var a = new SharedPrivateClass();
console.log("a:", a.getPrivate()); // "a: Default"

var b = new SharedPrivateClass();
console.log("b:", b.getPrivate()); // "b: Default"

a.setPrivate("foo"); // a Sets private to "foo"
console.log("a:", a.getPrivate()); // "a: foo"
console.log("b:", b.getPrivate()); // oh no, b.getPrivate() is "foo"!

console.log(a.hasOwnProperty("getPrivate")); // false. belongs to the prototype
console.log(a.private); // undefined

// getPrivate() is only created once and instanceof still works
console.log(a.getPrivate === b.getPrivate);
console.log(a instanceof SharedPrivateClass);
console.log(b instanceof SharedPrivateClass);

Hay muchos casos en los que esto sería adecuado, como si quisiera tener valores constantes como nombres de eventos que se comparten entre instancias. Pero esencialmente, actúan como variables privadas estáticas.

Si absolutamente necesita acceso a variables en un espacio de nombres privado desde sus métodos definidos en el prototipo, puede probar este patrón.

var PrivateNamespaceClass = (function() { // immediate function
    var instance = 0, // counts the number of instances
        defaultName = "Default Name",  
        p = []; // an array of private objects

    // create the constructor
    function PrivateNamespaceClass() {
        // Increment the instance count and save it to the instance. 
        // This will become your key to your private space.
        this.i = instance++; 
        
        // Create a new object in the private space.
        p[this.i] = {};
        // Define properties or methods in the private space.
        p[this.i].name = defaultName;
        
        console.log("New instance " + this.i);        
    }

    PrivateNamespaceClass.prototype.getPrivateName = function() {
        // It has access to the private space and it's children!
        return p[this.i].name;
    };
    PrivateNamespaceClass.prototype.setPrivateName = function(value) {
        // Because you use the instance number assigned to the object (this.i)
        // as a key, the values set will not change in other instances.
        p[this.i].name = value;
        return "Set " + p[this.i].name;
    };

    return PrivateNamespaceClass;
})();

var a = new PrivateNamespaceClass();
console.log(a.getPrivateName()); // Default Name

var b = new PrivateNamespaceClass();
console.log(b.getPrivateName()); // Default Name

console.log(a.setPrivateName("A"));
console.log(b.setPrivateName("B"));
console.log(a.getPrivateName()); // A
console.log(b.getPrivateName()); // B

// private objects are not accessible outside the PrivateNamespaceClass function
console.log(a.p);

// the prototype functions are not re-created for each instance
// and instanceof still works
console.log(a.getPrivateName === b.getPrivateName);
console.log(a instanceof PrivateNamespaceClass);
console.log(b instanceof PrivateNamespaceClass);

Me encantaría recibir comentarios de cualquiera que vea un error con esta forma de hacerlo.


44
Supongo que una posible preocupación es que cualquier instancia podría acceder a cualquier otro vars privado utilizando una identificación de instancia diferente. No necesariamente es algo malo ...
Mims H. Wright

15
Usted redefine las funciones prototipo en cada llamada de constructor
Lu4

10
@ Lu4 No estoy seguro de que sea cierto. El constructor se devuelve desde dentro de un cierre; la única vez que se definen las funciones prototipo es la primera vez, en esa expresión de función invocada inmediatamente. Dejando a un lado los problemas de privacidad mencionados anteriormente, esto me parece bien (a primera vista).
guypursey

1
@ MimsH.Wright otros idiomas permiten el acceso a otros objetos privados de la misma clase , pero solo cuando tiene referencia a ellos. Para permitir esto, podría ocultar los elementos privados detrás de una función que toma el puntero de los objetos como la clave (como se adjunta a una ID). De esa forma, solo tiene acceso a datos privados de objetos que conoce, lo que está más en línea con el alcance en otros idiomas. Sin embargo, esta implementación arroja luz sobre un problema más profundo con esto. Los objetos privados nunca se recolectarán como basura hasta que se active la función Constructor.
Thomas Nadin

3
Quiero mencionar que ise ha agregado a todas las instancias. Por lo tanto, no es completamente "transparente" y iaún podría ser manipulado.
Scott Rippey

18

ver la página de Doug Crockford sobre esto . Debe hacerlo indirectamente con algo que pueda acceder al alcance de la variable privada.

otro ejemplo:

Incrementer = function(init) {
  var counter = init || 0;  // "counter" is a private variable
  this._increment = function() { return counter++; }
  this._set = function(x) { counter = x; }
}
Incrementer.prototype.increment = function() { return this._increment(); }
Incrementer.prototype.set = function(x) { return this._set(x); }

caso de uso:

js>i = new Incrementer(100);
[object Object]
js>i.increment()
100
js>i.increment()
101
js>i.increment()
102
js>i.increment()
103
js>i.set(-44)
js>i.increment()
-44
js>i.increment()
-43
js>i.increment()
-42

47
Este ejemplo parece ser una práctica terrible. El objetivo de utilizar métodos prototipo es que no tenga que crear uno nuevo para cada instancia. Lo estás haciendo de todos modos. Para cada método estás creando otro.
Kir

2
@ArmedMonkey El concepto parece sólido, pero coincidió en que este es un mal ejemplo porque las funciones prototipo mostradas son triviales. Si las funciones prototipo fueran funciones mucho más largas que requieren un simple acceso get / set a las variables 'privadas', tendría sentido.
panqueque

9
Por qué se molestó exponiendo _seta través de set? ¿Por qué no solo nombrarlo setpara empezar?
Scott Rippey

15

Sugiero que probablemente sería una buena idea describir "tener una asignación de prototipo en un constructor" como un antipatrón Javascript. Piénsalo. Es demasiado arriesgado.

Lo que realmente está haciendo allí al crear el segundo objeto (es decir, b) es redefinir esa función prototipo para todos los objetos que usan ese prototipo. Esto restablecerá efectivamente el valor para el objeto a en su ejemplo. Funcionará si desea una variable compartida y si crea todas las instancias de objetos por adelantado, pero se siente demasiado arriesgado.

Encontré un error en algunos Javascript en los que estaba trabajando recientemente debido a este antipatrón exacto. Intentaba establecer un controlador de arrastrar y soltar en el objeto particular que se estaba creando, pero en cambio lo hacía para todas las instancias. No está bien.

La solución de Doug Crockford es la mejor.


10

@Kai

Eso no funcionará. Si lo haces

var t2 = new TestClass();

luego t2.prototypeHelloaccederá a la sección privada de t.

@AnglesCrimes

El código de muestra funciona bien, pero en realidad crea un miembro privado "estático" compartido por todas las instancias. Puede que no sea la solución que buscaban los morgancodes.

Hasta ahora no he encontrado una manera fácil y limpia de hacer esto sin introducir un hash privado y funciones de limpieza adicionales. Una función de miembro privado se puede simular en cierta medida:

(function() {
    function Foo() { ... }
    Foo.prototype.bar = function() {
       privateFoo.call(this, blah);
    };
    function privateFoo(blah) { 
        // scoped to the instance by passing this to call 
    }

    window.Foo = Foo;
}());

Entendió sus puntos claramente, pero ¿puede explicar qué intenta hacer su fragmento de código?
Vishwanath

privateFooes completamente privado y, por lo tanto, invisible al obtener un new Foo(). Solo bar()es un método público aquí, al que tiene acceso privateFoo. Puede usar el mismo mecanismo para variables y objetos simples, sin embargo, siempre debe tener en cuenta que estos privatesson realmente estáticos y serán compartidos por todos los objetos que cree.
Philzen

6

Si es posible. El patrón de diseño PPF solo resuelve esto.

PPF son las siglas de Private Prototype Functions. Basic PPF resuelve estos problemas:

  1. Las funciones prototipo obtienen acceso a datos de instancias privadas.
  2. Las funciones de prototipo pueden hacerse privadas.

Para el primero, solo:

  1. Coloque todas las variables de instancia privadas a las que desea acceder desde funciones prototipo dentro de un contenedor de datos separado, y
  2. Pase una referencia al contenedor de datos a todas las funciones del prototipo como parámetro.

Es así de simple. Por ejemplo:

// Helper class to store private data.
function Data() {};

// Object constructor
function Point(x, y)
{
  // container for private vars: all private vars go here
  // we want x, y be changeable via methods only
  var data = new Data;
  data.x = x;
  data.y = y;

  ...
}

// Prototype functions now have access to private instance data
Point.prototype.getX = function(data)
{
  return data.x;
}

Point.prototype.getY = function(data)
{
  return data.y;
}

...

Lee la historia completa aquí:

Patrón de diseño PPF


44
Las respuestas de solo enlace generalmente están mal vistas en SO. Por favor muestre un ejemplo.
Corey Adler

El artículo tiene ejemplos en su interior, así que por favor vea allí
Edward

55
Sin embargo, ¿qué sucede si en algún momento más tarde ese sitio deja de funcionar? ¿Cómo se supone que alguien vea un ejemplo entonces? La política está implementada para que cualquier cosa de valor en un enlace pueda mantenerse aquí, y no tenga que depender de un sitio web que no esté bajo nuestro control.
Corey Adler

3
@ Edward, ¡tu enlace es una lectura interesante! Sin embargo, me parece que la razón principal para acceder a datos privados mediante funciones prototípicas es evitar que cada objeto desperdicie memoria con funciones públicas idénticas. El método que describa no resuelve este problema, ya que para uso público, una función prototípica debe incluirse en una función pública normal. Supongo que el patrón podría ser útil para ahorrar memoria si tiene muchos ppf que se combinan en una sola función pública. ¿Los usas para otra cosa?
Dining Philosopher

@DiningPhilosofer, gracias por apreciar mi artículo. Sí, tiene razón, todavía utilizamos funciones de instancia. Pero la idea es tenerlos lo más livianos posible simplemente volviendo a llamar a sus contrapartes PPF que hacen todo el trabajo pesado. Finalmente, todas las instancias llaman a los mismos PPF (a través de envoltorios, por supuesto), por lo que se puede esperar un cierto ahorro de memoria. La pregunta es cuánto. Espero un ahorro sustancial.
Edward

5

En realidad, puede lograr esto utilizando la Verificación de Accesor :

(function(key, global) {
  // Creates a private data accessor function.
  function _(pData) {
    return function(aKey) {
      return aKey === key && pData;
    };
  }

  // Private data accessor verifier.  Verifies by making sure that the string
  // version of the function looks normal and that the toString function hasn't
  // been modified.  NOTE:  Verification can be duped if the rogue code replaces
  // Function.prototype.toString before this closure executes.
  function $(me) {
    if(me._ + '' == _asString && me._.toString === _toString) {
      return me._(key);
    }
  }
  var _asString = _({}) + '', _toString = _.toString;

  // Creates a Person class.
  var PersonPrototype = (global.Person = function(firstName, lastName) {
    this._ = _({
      firstName : firstName,
      lastName : lastName
    });
  }).prototype;
  PersonPrototype.getName = function() {
    var pData = $(this);
    return pData.firstName + ' ' + pData.lastName;
  };
  PersonPrototype.setFirstName = function(firstName) {
    var pData = $(this);
    pData.firstName = firstName;
    return this;
  };
  PersonPrototype.setLastName = function(lastName) {
    var pData = $(this);
    pData.lastName = lastName;
    return this;
  };
})({}, this);

var chris = new Person('Chris', 'West');
alert(chris.setFirstName('Christopher').setLastName('Webber').getName());

Este ejemplo proviene de mi publicación sobre Funciones prototípicas y datos privados y se explica con más detalle allí.


1
Esta respuesta es demasiado "inteligente" para ser útil, pero me gusta la respuesta de usar una variable vinculada a IFFE como un apretón de manos secreto. Esta implementación usa demasiados cierres para ser útil; el punto de tener métodos definidos prototipo es evitar la construcción de nuevos objetos de función para cada método en cada objeto.
greg.kindel

Este enfoque utiliza una clave secreta para identificar qué métodos prototipo son confiables y cuáles no. Sin embargo, es la instancia quien valida la clave, por lo que la clave debe enviarse a la instancia. Pero entonces, el código no confiable podría llamar a un método confiable en una instancia falsa, lo que robaría la clave. Y con esa clave, cree nuevos métodos que las instancias reales consideren confiables. Entonces esto es solo privacidad por oscuridad.
Oriol

4

En JavaScript actual, estoy bastante seguro de que hay una y solo una forma de tener un estado privado , accesible desde funciones prototipo , sin agregar nada público athis . La respuesta es usar el patrón de "mapa débil".

Para resumir: la Personclase tiene un solo mapa débil, donde las claves son las instancias de Person, y los valores son objetos simples que se utilizan para el almacenamiento privado.

Aquí hay un ejemplo completamente funcional: (jugar en http://jsfiddle.net/ScottRippey/BLNVr/ )

var Person = (function() {
    var _ = weakMap();
    // Now, _(this) returns an object, used for private storage.
    var Person = function(first, last) {
        // Assign private storage:
        _(this).firstName = first;
        _(this).lastName = last;
    }
    Person.prototype = {
        fullName: function() {
            // Retrieve private storage:
            return _(this).firstName + _(this).lastName;
        },
        firstName: function() {
            return _(this).firstName;
        },
        destroy: function() {
            // Free up the private storage:
            _(this, true);
        }
    };
    return Person;
})();

function weakMap() {
    var instances=[], values=[];
    return function(instance, destroy) {
        var index = instances.indexOf(instance);
        if (destroy) {
            // Delete the private state:
            instances.splice(index, 1);
            return values.splice(index, 1)[0];
        } else if (index === -1) {
            // Create the private state:
            instances.push(instance);
            values.push({});
            return values[values.length - 1];
        } else {
            // Return the private state:
            return values[index];
        }
    };
}

Como dije, esta es realmente la única forma de lograr las 3 partes.

Sin embargo, hay dos advertencias. Primero, esto cuesta rendimiento: cada vez que accede a los datos privados, es una O(n)operación, donde nestá el número de instancias. Por lo tanto, no querrá hacer esto si tiene una gran cantidad de instancias. En segundo lugar, cuando haya terminado con una instancia, debe llamardestroy ; de lo contrario, la instancia y los datos no se recolectarán basura, y terminará con una pérdida de memoria.

Y es por eso que mi respuesta original, "No deberías" , es algo a lo que me gustaría apegarme.


Si no destruye explícitamente una instancia de Persona antes de que salga del alcance, ¿el mapa débil no hace referencia a ella para que tenga una pérdida de memoria? Se me ocurrió un patrón para proteger, ya que otras instancias de Person pueden acceder a la variable y las que heredan de Person pueden. Simplemente lo resolví, así que no estoy seguro de si hay otras ventajas además del procesamiento adicional (no parece tanto como acceder a los privados) stackoverflow.com/a/21800194/1641941 Devolver un objeto privado / protegido es una molestia ya que llamar al código luego puede mutar su privado / protegido.
HMR

2
@HMR Sí, tienes que destruir explícitamente los datos privados. Agregaré esta advertencia a mi respuesta.
Scott Rippey

3

Hay una manera más simple al aprovechar el uso de bindycall métodos.

Al establecer variables privadas en un objeto, puede aprovechar el alcance de ese objeto.

Ejemplo

function TestClass (value) {
    // The private value(s)
    var _private = {
        value: value
    };

    // `bind` creates a copy of `getValue` when the object is instantiated
    this.getValue = TestClass.prototype.getValue.bind(_private);

    // Use `call` in another function if the prototype method will possibly change
    this.getValueDynamic = function() {
        return TestClass.prototype.getValue.call(_private);
    };
};

TestClass.prototype.getValue = function() {
    return this.value;
};

Este método no está exento de inconvenientes. Dado que el contexto del alcance se anula de manera efectiva, no tiene acceso fuera del _privateobjeto. Sin embargo, no es imposible dar acceso al alcance del objeto de instancia. Puede pasar el contexto del objeto ( this) como segundo argumento bindaocall para tener acceso a sus valores públicos en la función prototipo.

Accediendo a valores públicos

function TestClass (value) {
    var _private = {
        value: value
    };

    this.message = "Hello, ";

    this.getMessage = TestClass.prototype.getMessage.bind(_private, this);

}

TestClass.prototype.getMessage = function(_public) {

    // Can still access passed in arguments
    // e.g. – test.getValues('foo'), 'foo' is the 2nd argument to the method
    console.log([].slice.call(arguments, 1));
    return _public.message + this.value;
};

var test = new TestClass("World");
test.getMessage(1, 2, 3); // [1, 2, 3]         (console.log)
                          // => "Hello, World" (return value)

test.message = "Greetings, ";
test.getMessage(); // []                    (console.log)
                   // => "Greetings, World" (return value)

2
¿Por qué alguien crearía una copia del método prototipo en lugar de simplemente crear un método instanciado en primer lugar?
aplastar

3

¡Intentalo!

    function Potatoe(size) {
    var _image = new Image();
    _image.src = 'potatoe_'+size+'.png';
    function getImage() {
        if (getImage.caller == null || getImage.caller.owner != Potatoe.prototype)
            throw new Error('This is a private property.');
        return _image;
    }
    Object.defineProperty(this,'image',{
        configurable: false,
        enumerable: false,
        get : getImage          
    });
    Object.defineProperty(this,'size',{
        writable: false,
        configurable: false,
        enumerable: true,
        value : size            
    });
}
Potatoe.prototype.draw = function(ctx,x,y) {
    //ctx.drawImage(this.image,x,y);
    console.log(this.image);
}
Potatoe.prototype.draw.owner = Potatoe.prototype;

var pot = new Potatoe(32);
console.log('Potatoe size: '+pot.size);
try {
    console.log('Potatoe image: '+pot.image);
} catch(e) {
    console.log('Oops: '+e);
}
pot.draw();

1
Esto se basa en caller, que es una extensión dependiente de la implementación no permitida en modo estricto.
Oriol

1

Esto es lo que se me ocurrió.

(function () {
    var staticVar = 0;
    var yrObj = function () {
        var private = {"a":1,"b":2};
        var MyObj = function () {
            private.a += staticVar;
            staticVar++;
        };
        MyObj.prototype = {
            "test" : function () {
                console.log(private.a);
            }
        };

        return new MyObj;
    };
    window.YrObj = yrObj;
}());

var obj1 = new YrObj;
var obj2 = new YrObj;
obj1.test(); // 1
obj2.test(); // 2

El principal problema con esta implementación es que redefine los prototipos en cada instancia.


Interesante, me gusta mucho el intento y estaba pensando en lo mismo, pero tienes razón en que redefinir la función prototipo en cada instanciación es una limitación bastante grande. Esto no es solo porque se desperdician los ciclos de la CPU, sino porque si alguna vez cambia el prototipo más adelante, volvería a "restablecerse" a su estado original como se define en el constructor en la próxima instancia: /
Niko Bellic

1
Esto no solo redefinió los prototipos, sino que definió un nuevo constructor para cada instancia. Entonces las "instancias" ya no son instancias de la misma clase.
Oriol

1

Hay una manera muy simple de hacer esto

function SharedPrivate(){
  var private = "secret";
  this.constructor.prototype.getP = function(){return private}
  this.constructor.prototype.setP = function(v){ private = v;}
}

var o1 = new SharedPrivate();
var o2 = new SharedPrivate();

console.log(o1.getP()); // secret
console.log(o2.getP()); // secret
o1.setP("Pentax Full Frame K1 is on sale..!");
console.log(o1.getP()); // Pentax Full Frame K1 is on sale..!
console.log(o2.getP()); // Pentax Full Frame K1 is on sale..!
o2.setP("And it's only for $1,795._");
console.log(o1.getP()); // And it's only for $1,795._

Los prototipos de JavaScript son dorados.


2
Creo que es mejor no usar un prototipo en la función de constructor, ya que creará una nueva función cada vez que se cree una nueva instancia.
whamsicore

@whamsicore Sí, es cierto, pero en este caso es esencial, ya que para cada objeto instanciado tenemos que organizar un cierre compartido. Esa es la razón por la que las definiciones de funciones residen dentro del constructor y tenemos que hacer referencia a la SharedPrivate.prototypeque this.constructor.prototypees un gran problema para redefinir GETP y SETP varias veces ...
Redu

1

Llego tarde a la fiesta, pero creo que puedo contribuir. Aquí, mira esto:

// 1. Create closure
var SomeClass = function() {
  // 2. Create `key` inside a closure
  var key = {};
  // Function to create private storage
  var private = function() {
    var obj = {};
    // return Function to access private storage using `key`
    return function(testkey) {
      if(key === testkey) return obj;
      // If `key` is wrong, then storage cannot be accessed
      console.error('Cannot access private properties');
      return undefined;
    };
  };
  var SomeClass = function() {
    // 3. Create private storage
    this._ = private();
    // 4. Access private storage using the `key`
    this._(key).priv_prop = 200;
  };
  SomeClass.prototype.test = function() {
    console.log(this._(key).priv_prop); // Using property from prototype
  };
  return SomeClass;
}();

// Can access private property from within prototype
var instance = new SomeClass();
instance.test(); // `200` logged

// Cannot access private property from outside of the closure
var wrong_key = {};
instance._(wrong_key); // undefined; error logged

Llamo a este método patrón de acceso . La idea esencial es que tenemos un cierre , una clave dentro del cierre, y creamos un objeto privado (en el constructor) al que solo se puede acceder si tiene la clave .

Si está interesado, puede leer más sobre esto en mi artículo . Con este método, puede crear propiedades por objeto a las que no se puede acceder fuera del cierre. Por lo tanto, puede usarlos en constructor o prototipo, pero no en ningún otro lugar. No he visto este método usado en ningún lado, pero creo que es realmente poderoso.


0

¿No puedes poner las variables en un alcance más alto?

(function () {
    var privateVariable = true;

    var MyClass = function () {
        if (privateVariable) console.log('readable from private scope!');
    };

    MyClass.prototype.publicMethod = function () {
        if (privateVariable) console.log('readable from public scope!');
    };
}))();

44
Luego, las variables se comparten entre todas las instancias de MyClass.
aplastar

0

También puede intentar agregar el método no directamente en el prototipo, sino en la función de constructor como esta:

var MyArray = function() {
    var array = [];

    this.add = MyArray.add.bind(null, array);
    this.getAll = MyArray.getAll.bind(null, array);
}

MyArray.add = function(array, item) {
    array.push(item);
}
MyArray.getAll = function(array) {
    return array;
}

var myArray1 = new MyArray();
myArray1.add("some item 1");
console.log(myArray1.getAll()); // ['some item 1']
var myArray2 = new MyArray();
myArray2.add("some item 2");
console.log(myArray2.getAll()); // ['some item 2']
console.log(myArray1.getAll()); // ['some item 2'] - FINE!

0

Aquí hay algo que se me ocurrió al tratar de encontrar la solución más simple para este problema, tal vez podría ser útil para alguien. Soy nuevo en JavaScript, por lo que podría haber algunos problemas con el código.

// pseudo-class definition scope
(function () {

    // this is used to identify 'friend' functions defined within this scope,
    // while not being able to forge valid parameter for GetContext() 
    // to gain 'private' access from outside
    var _scope = new (function () { })();
    // -----------------------------------------------------------------

    // pseudo-class definition
    this.Something = function (x) {

        // 'private' members are wrapped into context object,
        // it can be also created with a function
        var _ctx = Object.seal({

            // actual private members
            Name: null,
            Number: null,

            Somefunc: function () {
                console.log('Something(' + this.Name + ').Somefunc(): number = ' + this.Number);
            }
        });
        // -----------------------------------------------------------------

        // function below needs to be defined in every class
        // to allow limited access from prototype
        this.GetContext = function (scope) {

            if (scope !== _scope) throw 'access';
            return _ctx;
        }
        // -----------------------------------------------------------------

        {
            // initialization code, if any
            _ctx.Name = (x !== 'undefined') ? x : 'default';
            _ctx.Number = 0;

            Object.freeze(this);
        }
    }
    // -----------------------------------------------------------------

    // prototype is defined only once
    this.Something.prototype = Object.freeze({

        // public accessors for 'private' field
        get Number() { return this.GetContext(_scope).Number; },
        set Number(v) { this.GetContext(_scope).Number = v; },

        // public function making use of some private fields
        Test: function () {

            var _ctx = this.GetContext(_scope);
            // access 'private' field
            console.log('Something(' + _ctx.Name + ').Test(): ' + _ctx.Number);
            // call 'private' func
            _ctx.Somefunc();
        }
    });
    // -----------------------------------------------------------------

    // wrap is used to hide _scope value and group definitions
}).call(this);

function _A(cond) { if (cond !== true) throw new Error('assert failed'); }
// -----------------------------------------------------------------

function test_smth() {

    console.clear();

    var smth1 = new Something('first'),
      smth2 = new Something('second');

    //_A(false);
    _A(smth1.Test === smth2.Test);

    smth1.Number = 3;
    smth2.Number = 5;
    console.log('smth1.Number: ' + smth1.Number + ', smth2.Number: ' + smth2.Number);

    smth1.Number = 2;
    smth2.Number = 6;

    smth1.Test();
    smth2.Test();

    try {
        var ctx = smth1.GetContext();
    } catch (err) {
        console.log('error: ' + err);
    }
}

test_smth();

0

Enfrenté exactamente la misma pregunta hoy y después de elaborar la respuesta de primera clase de Scott Rippey, se me ocurrió una solución muy simple (en mi humilde opinión) que es compatible con ES5 y eficiente, también es seguro para el choque de nombres (usar _private parece inseguro) .

/*jslint white: true, plusplus: true */

 /*global console */

var a, TestClass = (function(){
    "use strict";
    function PrefixedCounter (prefix) {
        var counter = 0;
        this.count = function () {
            return prefix + (++counter);
        };
    }
    var TestClass = (function(){
        var cls, pc = new PrefixedCounter("_TestClass_priv_")
        , privateField = pc.count()
        ;
        cls = function(){
            this[privateField] = "hello";
            this.nonProtoHello = function(){
                console.log(this[privateField]);
            };
        };
        cls.prototype.prototypeHello = function(){
            console.log(this[privateField]);
        };
        return cls;
    }());
    return TestClass;
}());

a = new TestClass();
a.nonProtoHello();
a.prototypeHello();

Probado con ringojs y nodejs. Estoy ansioso por leer tu opinión.


Aquí hay una referencia: consulte la sección 'Un paso más cerca'. philipwalton.com/articles/…
jimasun

0
var getParams = function(_func) {
  res = _func.toString().split('function (')[1].split(')')[0].split(',')
  return res
}

function TestClass(){

  var private = {hidden: 'secret'}
  //clever magic accessor thing goes here
  if ( !(this instanceof arguments.callee) ) {
    for (var key in arguments) {
      if (typeof arguments[key] == 'function') {
        var keys = getParams(arguments[key])
        var params = []
        for (var i = 0; i <= keys.length; i++) {
          if (private[keys[i]] != undefined) {
            params.push(private[keys[i]])
          }
        }
        arguments[key].apply(null,params)
      }
    }
  }
}


TestClass.prototype.test = function(){
  var _hidden; //variable I want to get
  TestClass(function(hidden) {_hidden = hidden}) //invoke magic to get
};

new TestClass().test()

¿Cómo es esto? Usando un descriptor de acceso privado. Solo le permite obtener las variables, aunque no establecerlas, depende del caso de uso.


Esto no responde la pregunta de una manera útil. ¿ Por qué crees que esta es la respuesta? como funciona Simplemente decirle a alguien que cambie su código sin ningún contexto o significado no les ayuda a aprender lo que hicieron mal.
GrumpyCrouton

Quería una forma de acceder a las variables privadas ocultas de una Clase a través de prototipos sin tener que crear esa variable oculta en cada instancia de la clase. El código anterior es un método de ejemplo para hacer eso. ¿Cómo es que no es una respuesta a la pregunta?
dylan0150

No dije que no era una respuesta a la pregunta. Dije que no era una respuesta útil , porque no ayuda a nadie a aprender. Debe explicar su código, por qué funciona, por qué es la forma correcta de hacerlo. Si yo fuera el autor de la pregunta, no aceptaría su respuesta porque no fomenta el aprendizaje, no me enseña qué estoy haciendo mal o qué está haciendo el código dado o cómo funciona.
GrumpyCrouton

0

Tengo una solución, pero no estoy seguro de que no tenga fallas.

Para que funcione, debe usar la siguiente estructura:

  1. Use 1 objeto privado que contenga todas las variables privadas.
  2. Utilice 1 función de instancia.
  3. Aplique un cierre al constructor y a todas las funciones prototipo.
  4. Cualquier instancia creada se realiza fuera del cierre definido.

Aquí está el código:

var TestClass = 
(function () {
    // difficult to be guessed.
    var hash = Math.round(Math.random() * Math.pow(10, 13) + + new Date());
    var TestClass = function () {
        var privateFields = {
            field1: 1,
            field2: 2
        };
        this.getPrivateFields = function (hashed) {
            if(hashed !== hash) {
                throw "Cannot access private fields outside of object.";
                // or return null;
            }
            return privateFields;
        };
    };

    TestClass.prototype.prototypeHello = function () {
        var privateFields = this.getPrivateFields(hash);
        privateFields.field1 = Math.round(Math.random() * 100);
        privateFields.field2 = Math.round(Math.random() * 100);
    };

    TestClass.prototype.logField1 = function () {
        var privateFields = this.getPrivateFields(hash);
        console.log(privateFields.field1);
    };

    TestClass.prototype.logField2 = function () {
        var privateFields = this.getPrivateFields(hash);
        console.log(privateFields.field2);
    };

    return TestClass;
})();

Cómo funciona esto es que proporciona una función de instancia "this.getPrivateFields" para acceder al objeto de variables privadas "privateFields", pero esta función solo devolverá el objeto "privateFields" dentro del cierre principal definido (también funciones prototipo que usan "this.getPrivateFields" "debe definirse dentro de este cierre).

Un hash producido durante el tiempo de ejecución y difícil de adivinar se usa como parámetro para asegurarse de que incluso si se llama "getPrivateFields" fuera del alcance del cierre, no se devolverá el objeto "privateFields".

El inconveniente es que no podemos extender TestClass con más funciones prototipo fuera del cierre.

Aquí hay un código de prueba:

var t1 = new TestClass();
console.log('Initial t1 field1 is: ');
t1.logField1();
console.log('Initial t1 field2 is: ');
t1.logField2();
t1.prototypeHello();
console.log('t1 field1 is now: ');
t1.logField1();
console.log('t1 field2 is now: ');
t1.logField2();
var t2 = new TestClass();
console.log('Initial t2 field1 is: ');
t2.logField1();
console.log('Initial t2 field2 is: ');
t2.logField2();
t2.prototypeHello();
console.log('t2 field1 is now: ');
t2.logField1();
console.log('t2 field2 is now: ');
t2.logField2();

console.log('t1 field1 stays: ');
t1.logField1();
console.log('t1 field2 stays: ');
t1.logField2();

t1.getPrivateFields(11233);

EDITAR: Usando este método, también es posible "definir" funciones privadas.

TestClass.prototype.privateFunction = function (hashed) {
    if(hashed !== hash) {
        throw "Cannot access private function.";
    }
};

TestClass.prototype.prototypeHello = function () {
    this.privateFunction(hash);
};

0

Estaba jugando con esto hoy y esta fue la única solución que pude encontrar sin usar Símbolos. Lo mejor de esto es que en realidad puede ser completamente privado.

La solución se basa en un cargador de módulos de cosecha propia que básicamente se convierte en el mediador de un caché de almacenamiento privado (usando un mapa débil).

   const loader = (function() {
        function ModuleLoader() {}

    //Static, accessible only if truly needed through obj.constructor.modules
    //Can also be made completely private by removing the ModuleLoader prefix.
    ModuleLoader.modulesLoaded = 0;
    ModuleLoader.modules = {}

    ModuleLoader.prototype.define = function(moduleName, dModule) {
        if (moduleName in ModuleLoader.modules) throw new Error('Error, duplicate module');

        const module = ModuleLoader.modules[moduleName] = {}

        module.context = {
            __moduleName: moduleName,
            exports: {}
        }

        //Weak map with instance as the key, when the created instance is garbage collected or goes out of scope this will be cleaned up.
        module._private = {
            private_sections: new WeakMap(),
            instances: []
        };

        function private(action, instance) {
            switch (action) {
                case "create":
                    if (module._private.private_sections.has(instance)) throw new Error('Cannot create private store twice on the same instance! check calls to create.')
                    module._private.instances.push(instance);
                    module._private.private_sections.set(instance, {});
                    break;
                case "delete":
                    const index = module._private.instances.indexOf(instance);
                    if (index == -1) throw new Error('Invalid state');
                    module._private.instances.slice(index, 1);
                    return module._private.private_sections.delete(instance);
                    break;
                case "get":
                    return module._private.private_sections.get(instance);
                    break;
                default:
                    throw new Error('Invalid action');
                    break;
            }
        }

        dModule.call(module.context, private);
        ModuleLoader.modulesLoaded++;
    }

    ModuleLoader.prototype.remove = function(moduleName) {
        if (!moduleName in (ModuleLoader.modules)) return;

        /*
            Clean up as best we can.
        */
        const module = ModuleLoader.modules[moduleName];
        module.context.__moduleName = null;
        module.context.exports = null;
        module.cotext = null;
        module._private.instances.forEach(function(instance) { module._private.private_sections.delete(instance) });
        for (let i = 0; i < module._private.instances.length; i++) {
            module._private.instances[i] = undefined;
        }
        module._private.instances = undefined;
        module._private = null;
        delete ModuleLoader.modules[moduleName];
        ModuleLoader.modulesLoaded -= 1;
    }


    ModuleLoader.prototype.require = function(moduleName) {
        if (!(moduleName in ModuleLoader.modules)) throw new Error('Module does not exist');

        return ModuleLoader.modules[moduleName].context.exports;
    }



     return new ModuleLoader();
    })();

    loader.define('MyModule', function(private_store) {
        function MyClass() {
            //Creates the private storage facility. Called once in constructor.
            private_store("create", this);


            //Retrieve the private storage object from the storage facility.
            private_store("get", this).no = 1;
        }

        MyClass.prototype.incrementPrivateVar = function() {
            private_store("get", this).no += 1;
        }

        MyClass.prototype.getPrivateVar = function() {
            return private_store("get", this).no;
        }

        this.exports = MyClass;
    })

    //Get whatever is exported from MyModule
    const MyClass = loader.require('MyModule');

    //Create a new instance of `MyClass`
    const myClass = new MyClass();

    //Create another instance of `MyClass`
    const myClass2 = new MyClass();

    //print out current private vars
    console.log('pVar = ' + myClass.getPrivateVar())
    console.log('pVar2 = ' + myClass2.getPrivateVar())

    //Increment it
    myClass.incrementPrivateVar()

    //Print out to see if one affected the other or shared
    console.log('pVar after increment = ' + myClass.getPrivateVar())
    console.log('pVar after increment on other class = ' + myClass2.getPrivateVar())

    //Clean up.
    loader.remove('MyModule')

0

Sé que ha pasado más de una década desde que me lo pidieron, pero solo pensé en esto por enésima vez en mi vida de programador, y encontré una posible solución que no sé si todavía me gusta por completo. . No he visto esta metodología documentada antes, así que la llamaré el "patrón de dólar público / privado" o el patrón _ $ / $ .

var ownFunctionResult = this.$("functionName"[, arg1[, arg2 ...]]);
var ownFieldValue = this._$("fieldName"[, newValue]);

var objectFunctionResult = objectX.$("functionName"[, arg1[, arg2 ...]]);

//Throws an exception. objectX._$ is not defined
var objectFieldValue = objectX._$("fieldName"[, newValue]);

El concepto utiliza una función ClassDefinition que devuelve una función Constructor que devuelve un objeto Interface . El único método de la interfaz es el $que recibe un nameargumento para invocar la función correspondiente en el objeto constructor, cualquier argumento adicional pasado despuésname se pasa en la invocación.

La función auxiliar definida globalmente ClassValuesalmacena todos los campos en un objeto según sea necesario. Define la _$función para acceder a ellos name. Esto sigue un breve patrón get / set, por lo que si valuese pasa, se usará como el nuevo valor de la variable.

var ClassValues = function (values) {
  return {
    _$: function _$(name, value) {
      if (arguments.length > 1) {
        values[name] = value;
      }

      return values[name];
    }
  };
};

La función definida globalmente Interfacetoma un objeto y un Valuesobjeto para devolver un _interfacecon una sola función $que examina objpara encontrar una función con el nombre del parámetro namey la invoca valuescomo el objeto con ámbito . Los argumentos adicionales pasados $se pasarán en la invocación de la función.

var Interface = function (obj, values, className) {
  var _interface = {
    $: function $(name) {
      if (typeof(obj[name]) === "function") {
        return obj[name].apply(values, Array.prototype.splice.call(arguments, 1));
      }

      throw className + "." + name + " is not a function.";
    }
  };

  //Give values access to the interface.
  values.$ = _interface.$;

  return _interface;
};

En la siguiente muestra, ClassXse asigna al resultado de ClassDefinition, que es la Constructorfunción. Constructorpuede recibir cualquier cantidad de argumentos. Interfacees lo que obtiene el código externo después de llamar al constructor.

var ClassX = (function ClassDefinition () {
  var Constructor = function Constructor (valA) {
    return Interface(this, ClassValues({ valA: valA }), "ClassX");
  };

  Constructor.prototype.getValA = function getValA() {
    //private value access pattern to get current value.
    return this._$("valA");
  };

  Constructor.prototype.setValA = function setValA(valA) {
    //private value access pattern to set new value.
    this._$("valA", valA);
  };

  Constructor.prototype.isValAValid = function isValAValid(validMessage, invalidMessage) {
    //interface access pattern to call object function.
    var valA = this.$("getValA");

    //timesAccessed was not defined in constructor but can be added later...
    var timesAccessed = this._$("timesAccessed");

    if (timesAccessed) {
      timesAccessed = timesAccessed + 1;
    } else {
      timesAccessed = 1;
    }

    this._$("timesAccessed", timesAccessed);

    if (valA) {
      return "valA is " + validMessage + ".";
    }

    return "valA is " + invalidMessage + ".";
  };

  return Constructor;
}());

No tiene sentido tener funciones no prototipadas Constructor, aunque podría definirlas en el cuerpo de la función del constructor. Todas las funciones se llaman con el patrón de dólar público this.$("functionName"[, param1[, param2 ...]]) . Se accede a los valores privados con el patrón de dólar privado this._$("valueName"[, replacingValue]); . Como Interfaceno tiene una definición para _$, los objetos externos no pueden acceder a los valores. Dado que cada cuerpo de función prototipada thisse establece en el valuesobjeto en función $, obtendrá excepciones si llama directamente a las funciones hermanas Constructor; El patrón _ $ / $ también debe seguirse en los cuerpos de función prototipados. Debajo de la muestra de uso.

var classX1 = new ClassX();
console.log("classX1." + classX1.$("isValAValid", "valid", "invalid"));
console.log("classX1.valA: " + classX1.$("getValA"));
classX1.$("setValA", "v1");
console.log("classX1." + classX1.$("isValAValid", "valid", "invalid"));
var classX2 = new ClassX("v2");
console.log("classX1.valA: " + classX1.$("getValA"));
console.log("classX2.valA: " + classX2.$("getValA"));
//This will throw an exception
//classX1._$("valA");

Y la salida de la consola.

classX1.valA is invalid.
classX1.valA: undefined
classX1.valA is valid.
classX1.valA: v1
classX2.valA: v2

El patrón _ $ / $ permite la total privacidad de los valores en clases completamente prototipadas. No sé si alguna vez usaré esto, ni si tiene fallas, pero bueno, ¡fue un buen rompecabezas!


0

ES6 WeakMaps

Mediante el uso de un patrón simple basado en ES6, WeakMaps es posible obtener variables de miembros privados, accesibles desde las funciones prototipo .

Nota: El uso de WeakMaps garantiza la seguridad contra pérdidas de memoria , al permitir que el recolector de basura identifique y descarte las instancias no utilizadas.

// Create a private scope using an Immediately 
// Invoked Function Expression...
let Person = (function() {

    // Create the WeakMap that will hold each  
    // Instance collection's of private data
    let privateData = new WeakMap();
    
    // Declare the Constructor :
    function Person(name) {
        // Insert the private data in the WeakMap,
        // using 'this' as a unique acces Key
        privateData.set(this, { name: name });
    }
    
    // Declare a prototype method 
    Person.prototype.getName = function() {
        // Because 'privateData' is in the same 
        // scope, it's contents can be retrieved...
        // by using  again 'this' , as  the acces key 
        return privateData.get(this).name;
    };

    // return the Constructor
    return Person;
}());

Una explicación más detallada de este patrón se puede encontrar aquí.


-1

Necesita cambiar 3 cosas en su código:

  1. Reemplazar var privateField = "hello"conthis.privateField = "hello" .
  2. En el prototipo reemplazar privateFieldconthis.privateField .
  3. En el no prototipo también reemplazar privateFieldcon this.privateField.

El código final sería el siguiente:

TestClass = function(){
    this.privateField = "hello";
    this.nonProtoHello = function(){alert(this.privateField)};
}

TestClass.prototype.prototypeHello = function(){alert(this.privateField)};

var t = new TestClass();

t.prototypeHello()

this.privateFieldNo sería un campo privado. es accesible desde el exterior:t.privateField
V. Rubinetti

-2

Puede usar una asignación de prototipo dentro de la definición del constructor.

La variable será visible para el método agregado del prototipo, pero todas las instancias de las funciones accederán a la misma variable COMPARTIDA.

function A()
{
  var sharedVar = 0;
  this.local = "";

  A.prototype.increment = function(lval)
  {    
    if (lval) this.local = lval;    
    alert((++sharedVar) + " while this.p is still " + this.local);
  }
}

var a = new A();
var b = new A();    
a.increment("I belong to a");
b.increment("I belong to b");
a.increment();
b.increment();

Espero que esto pueda ser útil.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.