¿Por qué puedo acceder a miembros privados de TypeScript cuando no debería poder hacerlo?

108

Estoy buscando la implementación de miembros privados en TypeScript y lo encuentro un poco confuso. Intellisense no permite acceder a miembros privados, pero en JavaScript puro, está todo ahí. Esto me hace pensar que TS no implementa a los miembros privados correctamente. ¿Alguna idea?

class Test{
  private member: any = "private member";
}
alert(new Test().member);
Sean Feldman
fuente
¿Se pregunta por qué IntelliSense no le da el miembro privado en la fila con la alerta ()?
esrange
7
No Me pregunto por qué TS tiene un privado cuando eso es solo un azúcar para intellisense, y no realmente para el JavaScript en el que se compila. Este código ejecutado en typescriptlang.org/Playground alerta sobre el valor del miembro privado.
Sean Feldman
Como se mencionó, debe declarar elementos como una variable en un contexto privado para que sean privados. Supongo que el mecanografiado no hace esto porque puede ser ineficiente en comparación con la adición al prototipo. También interfiere con la definición de tipo (los miembros privados no son realmente parte de la clase)
Shane
Si desea variables privadas reales que existan en el prototipo, requiere algunos gastos generales, pero he escrito una biblioteca llamada ClassJS que hace precisamente eso en GitHub: github.com/KthProg/ClassJS .
KthProg

Respuestas:

97

Al igual que con la verificación de tipos, la privacidad de los miembros solo se aplica dentro del compilador.

Una propiedad privada se implementa como una propiedad regular y el código fuera de la clase no puede acceder a ella.

Para hacer algo realmente privado dentro de la clase, no puede ser un miembro de la clase, sería una variable local creada dentro de un alcance de función dentro del código que crea el objeto. Eso significaría que no puede acceder a él como un miembro de la clase, es decir, usando la thispalabra clave.

Guffa
fuente
25
No es inusual que un programador de JavaScript coloque una variable local en un constructor de objetos y la use como un campo privado. Me sorprende que no apoyaran algo como esto.
Eric
2
@Eric: como TypeScript usa el prototipo para métodos en lugar de agregar métodos como prototipos dentro del constructor, no se puede acceder a una variable local en el constructor desde los métodos. Podría ser posible crear una variable local dentro del contenedor de la función para la clase, pero aún no he encontrado una manera de hacerlo. Sin embargo, seguiría siendo una variable local y no un miembro privado.
Guffa
40
Esto es algo sobre lo que he estado proporcionando comentarios. Creo que debería ofrecer la opción de crear un patrón de módulo revelador, para que los miembros privados puedan permanecer privados y los públicos puedan ser accesibles en JavaScript. Este es un patrón común y proporcionaría la misma accesibilidad en TS y JS.
John Papa
Hay una solución que puede usar para miembros estáticos privados: basarat.com/2013/03/real-private-static-class-members-in.html
basarat
1
@BasaratAli: Esa es una variable estática que está disponible dentro de los métodos de la clase, pero no es un miembro de la clase, es decir, no se accede a ella usando la thispalabra clave.
Guffa
37

JavaScript admite variables privadas.

function MyClass() {
    var myPrivateVar = 3;

    this.doSomething = function() {
        return myPrivateVar++;        
    }
}

En TypeScript esto se expresaría así:

class MyClass {

    doSomething: () => number;

    constructor() {
        var myPrivateVar = 3;

        this.doSomething = function () {
            return myPrivateVar++;
        }
    }
}

EDITAR

Este enfoque solo debe usarse CON ESPACIO donde sea absolutamente necesario. Por ejemplo, si necesita almacenar una contraseña en caché temporalmente.

Hay costos de rendimiento para usar este patrón (irrelevante de Javascript o TypeScript) y solo debe usarse donde sea absolutamente necesario.

Martín
fuente
¿No hace esto mecanografiado TODO el tiempo configurando var _thissu uso en funciones de ámbito? ¿Por qué tendrías reparos en hacerlo en el ámbito de la clase?
DrSammyD
No. var _this es solo una referencia a esto.
Martin
2
Más exactamente para llamarlas variables constructoras, no privadas. Aquellos no son visibles en los métodos de prototipos.
Roman M. Koss
1
Oh, sí, lo siento, el problema fue otro en su lugar, el hecho de que para cada instancia que cree, doSomething se creará nuevamente, porque no es parte de la cadena de prototipos.
Barbu Barbu
1
@BarbuBarbu Sí, estoy de acuerdo. Este es un GRAN problema con este enfoque, y una de las razones por las que debe evitarse.
Martin
11

Una vez que el soporte para WeakMap esté más disponible, habrá una técnica interesante detallada en el ejemplo n. ° 3 aquí. .

Permite datos privados Y evita los costos de rendimiento del ejemplo de Jason Evans al permitir que los datos sean accesibles desde métodos prototipo en lugar de solo métodos de instancia.

La página vinculada MDN WeakMap enumera la compatibilidad del navegador en Chrome 36, Firefox 6.0, IE 11, Opera 23 y Safari 7.1.

let _counter = new WeakMap();
let _action = new WeakMap();
class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  decrement() {
    let counter = _counter.get(this);
    if (counter < 1) return;
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
      _action.get(this)();
    }
  }
}
Ryan Thomas
fuente
¡Me gustó! Básicamente significa ocultar propiedades privadas en clases agregadas. Lo más divertido será ... ¿Qué tal si agregamos soporte para protectedparámetros? : D
Roman M. Koss
2
@RamtinSoltani Las estadísticas del artículo vinculado que, debido a cómo funcionan los mapas débiles, esto no evitará la recolección de basura. Si alguien quisiera estar más seguro al usar esta técnica, podría implementar su propio código de eliminación que elimine la clave de instancia de clase de cada uno de los mapas débiles.
Ryan Thomas
1
Desde la página de MDN: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… . Por el contrario, los WeakMaps nativos contienen referencias "débiles" a objetos clave, lo que significa que no impiden la recolección de basura en caso de que no haya otra referencia al objeto clave. Esto también evita evitar la recolección de basura de valores en el mapa.
Ryan Thomas
@RyanThomas Es cierto, ese fue un comentario antiguo que dejé hace un tiempo. Los WeakMaps, a diferencia de los mapas, no causarán pérdidas de memoria. Entonces es seguro usar esta técnica.
Ramtin Soltani
@RamtinSoltani ¿Entonces borras tu comentario anterior?>
ErikE
10

Dado que se lanzará TypeScript 3.8, podrá declarar un campo privado al que no se puede acceder o incluso detectar fuera de la clase que lo contiene .

class Person {
    #name: string

    constructor(name: string) {
        this.#name = name;
    }

    greet() {
        console.log(`Hello, my name is ${this.#name}!`);
    }
}

let jeremy = new Person("Jeremy Bearimy");

jeremy.#name
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

Los campos privados comienzan con # carácter

Tenga en cuenta que estos campos privados serán diferentes a los campos marcados con private palabra clave

Árbitro. https://devblogs.microsoft.com/typescript/announcing-typescript-3-8-beta/

Przemek Struciński
fuente
4

Gracias a Sean Feldman por el enlace a la discusión oficial sobre este tema; vea su respuesta para el enlace.

Leí la discusión a la que se vinculó y aquí hay un resumen de los puntos clave:

  • Sugerencia: propiedades privadas en constructor
    • problemas: no se puede acceder desde las funciones del prototipo
  • Sugerencia: métodos privados en el constructor
    • problemas: lo mismo que con las propiedades, además de perder el beneficio de rendimiento de crear una función una vez por clase en el prototipo; en su lugar, crea una copia de la función para cada instancia
  • Sugerencia: agregue un texto estándar al acceso a la propiedad abstracta y haga cumplir la visibilidad
    • problemas: gastos generales importantes de rendimiento; TypeScript está diseñado para aplicaciones grandes
  • Sugerencia: TypeScript ya envuelve las definiciones de método de constructor y prototipo en un cierre; poner métodos privados y propiedades allí
    • problemas para poner propiedades privadas en ese cierre: se convierten en variables estáticas; no hay uno por instancia
    • problemas para poner métodos privados en ese cierre: no tienen acceso thissin algún tipo de solución
  • Sugerencia: modifica automáticamente los nombres de las variables privadas
    • argumentos en contra: eso es una convención de nomenclatura, no una construcción del lenguaje. Destrúyelo tú mismo
  • Sugerencia: anote métodos privados con@private minificadores que reconozcan que la anotación puede minificar eficazmente los nombres de los métodos
    • No hay argumentos contrarios significativos a este

Contraargumentos generales para agregar soporte de visibilidad en el código emitido:

  • el problema es que JavaScript en sí no tiene modificadores de visibilidad; este no es un problema de TypeScript
  • ya existe un patrón establecido en la comunidad de JavaScript: anteponer propiedades y métodos privados con un guión bajo, que dice "proceda bajo su propio riesgo"
  • cuando los diseñadores de TypeScript dijeron que las propiedades y métodos verdaderamente privados no son "posibles", querían decir "no es posible bajo nuestras restricciones de diseño", específicamente:
    • El JS emitido es idiomático
    • La placa de caldera es mínima
    • Sin gastos generales adicionales en comparación con JS OOP normal
alexanderbird
fuente
Si esta respuesta fue de esta conversación: typescript.codeplex.com/discussions/397651 -, proporcione un enlace: D
Roman M. Koss
1
Sí, esa es la conversación, pero me vinculé a la respuesta de Sean Feldman a esta pregunta , donde proporciona el enlace. Dado que él hizo el trabajo de encontrar el enlace, quería darle el crédito.
alexanderbird
2

En TypeScript, las funciones privadas solo son accesibles dentro de la clase. Me gusta

ingrese la descripción de la imagen aquí

Y mostrará un error cuando intente acceder a un miembro privado. Aquí está el ejemplo:

ingrese la descripción de la imagen aquí

Nota: estará bien con javascript y ambas funciones son accesibles desde el exterior.

Muhammad Awais
fuente
4
OP: "pero en JavaScript puro, todo está ahí" - No creo que aborde el problema de que el JavaScript generado expone públicamente las funciones "privadas"
alexanderbird
1
@alexanderbird Creo que quería decir que TypeScript suele ser suficiente. Cuando desarrollamos en TypeScript, nos quedamos con él en el alcance del proyecto, por lo que la privacidad de JavaScript no es un gran problema. Porque, en primer lugar, el código original es importante para el desarrollador, no los transpilados (JavaScript).
Roman M. Koss
1
A menos que esté escribiendo y publicando una biblioteca de JavaScript, entonces el código transpilado sí importa
alexanderbird
su respuesta está fuera de tema.
canbax
1

Me doy cuenta de que esta es una discusión anterior, pero aún podría ser útil compartir mi solución al problema de las variables y métodos supuestamente privados en una "filtración" de TypeScript hacia la interfaz pública de la clase JavaScript compilada.

Para mí, este problema es puramente cosmético, es decir, se trata del desorden visual cuando se ve una variable de instancia en DevTools. Mi solución es agrupar las declaraciones privadas dentro de otra clase que luego se instancia en la clase principal y se asigna a una privatevariable (pero aún visible públicamente en JS) con un nombre como__ (doble subrayado).

Ejemplo:

class Privates {
    readonly DEFAULT_MULTIPLIER = 2;
    foo: number;
    bar: number;

    someMethod = (multiplier: number = this.DEFAULT_MULTIPLIER) => {
        return multiplier * (this.foo + this.bar);
    }

    private _class: MyClass;

    constructor(_class: MyClass) {
        this._class = _class;
    }
}

export class MyClass {
    private __: Privates = new Privates(this);

    constructor(foo: number, bar: number, baz: number) {
        // assign private property values...
        this.__.foo = foo;
        this.__.bar = bar;

        // assign public property values...
        this.baz = baz;
    }

    baz: number;

    print = () => {
        console.log(`foo=${this.__.foo}, bar=${this.__.bar}`);
        console.log(`someMethod returns ${this.__.someMethod()}`);
    }
}

let myClass = new MyClass(1, 2, 3);

Cuando la myClassinstancia se ve en DevTools, en lugar de ver todos sus miembros "privados" entremezclados con los verdaderamente públicos (lo que puede resultar muy desordenado visualmente en el código de la vida real correctamente refactorizado), los verá agrupados ordenadamente dentro de la __propiedad contraída :

ingrese la descripción de la imagen aquí

Canuck del Caspio
fuente
1
Me gusta. Parece limpio.
0

Este es un enfoque reutilizable para agregar propiedades privadas adecuadas:

/**
 * Implements proper private properties.
 */
export class Private<K extends object, V> {

    private propMap = new WeakMap<K, V>();

    get(obj: K): V {
        return this.propMap.get(obj)!;
    }

    set(obj: K, val: V) {
        this.propMap.set(obj, val);
    }
}

Digamos que tienes una clase en Clientalgún lugar que necesita dos propiedades privadas:

  • prop1: string
  • prop2: number

A continuación se muestra cómo lo implementa:

// our private properties:
interface ClientPrivate {
    prop1: string;
    prop2: number;
}

// private properties for all Client instances:
const pp = new Private<Client, ClientPrivate>();

class Client {
    constructor() {
        pp.set(this, {
            prop1: 'hello',
            prop2: 123
        });
    }

    someMethod() {
        const privateProps = pp.get(this);

        const prop1 = privateProps.prop1;
        const prop2 = privateProps.prop2;
    }
}

Y si todo lo que necesita es una sola propiedad privada, entonces se vuelve aún más simple, porque no necesitaría definir ninguna ClientPrivate en ese caso.

Vale la pena señalar que, en su mayor parte, la clase Privatesolo ofrece una firma bien legible, mientras que el uso directo WeakMapno lo hace.

vitaly-t
fuente