Post Top Ad

Your Ad Spot

domingo, 28 de junio de 2020

Propuesta de ECMAScript: campos de clase privada

Esta publicación de blog es parte de una serie sobre nuevos miembros en cuerpos de definiciones de clase:
  1. Campos de clase publica
  2. Campos de clases privadas
  3. Métodos de prototipos privados y captadores / establecedores en clases
  4. Métodos estáticos privados y captadores / establecedores en clases

En esta publicación, observamos los campos privados , un nuevo tipo de espacio privado en instancias y clases. Esta característica es parte de la propuesta de ES "Declaraciones de campo de clase para JavaScript" de Daniel Ehrenberg y Jeff Morrison.

Tabla de contenido:

Resumen   

Los campos privados son un nuevo tipo de ranura de datos que es diferente de las propiedades. Solo se puede acceder directamente dentro del cuerpo de la clase en la que se declaran.

Campos estáticos privados   

class MyClass {
  // Declare and initialize
  static #privateStaticField = 1;
  static getPrivateStaticField() {
    return MyClass.#privateStaticField; // (A)
  }
}
assert.throws(
  () => eval('MyClass.#privateStaticField'),
  {
    name: 'SyntaxError',
    message: 'Private field \'#privateStaticField\'' +
      ' must be declared in an enclosing class',
  }
);
assert.equal(MyClass.getPrivateStaticField(), 1);
El 'campo privado' #privateStaticField 'debe declararse en una clase adjunta'
Consejo: Nunca use thispara acceder a un campo estático privado, use siempre el nombre de clase directa (como en la línea A). Por qué se explica más adelante en esta publicación .

Campos de instancia privada   

Uso de campos privados con inicializadores (signos iguales seguidos de valores):
class MyClass {
  // Declare and initialize
  #privateInstanceField = 2;
  getPrivateInstanceField() {
    return this.#privateInstanceField;
  }
}
assert.throws(
  () => eval('new MyClass().#privateInstanceField'),
  {
    name: 'SyntaxError',
    message: 'Private field \'#privateInstanceField\'' +
      ' must be declared in an enclosing class',
  }
);
assert.equal(new MyClass().getPrivateInstanceField(), 2);
Usar campos de instancia privados sin inicializadores:
class DataStore {
  #data; // must be declared
  constructor(data) {
    this.#data = data;
  }
  getData() {
    return this.#data;
  }
}
assert.deepEqual(
  Reflect.ownKeys(new DataStore()),
  []);

De guiones bajos a campos de instancias privadas   

Una técnica común para mantener los datos privados en JavaScript es prefijar los nombres de las propiedades con guiones bajos. En esta sección, comenzaremos con el código que usa esta técnica y luego lo cambiaremos, para que use campos de instancia privados.

Comenzando con guiones bajos   

class Countdown {
  constructor(counter, action) {
    this._counter = counter; // private
    this._action = action; // private
  }
  dec() {
    if (this._counter < 1) return;
    this._counter--;
    if (this._counter === 0) {
      this._action();
    }
  }
}
// The data is not really private:
assert.deepEqual(
  Reflect.ownKeys(new Countdown(5, () => {})),
  ['_counter', '_action']);
Esta técnica no nos da ninguna protección; simplemente sugiere a las personas que usan esta clase: no toquen estas propiedades, se consideran privadas.
El principal beneficio de esta técnica es que es conveniente. Con los campos privados, no perdemos la comodidad y obtenemos una verdadera privacidad.

Cambiar a campos de instancias privadas   

Podemos cambiar de guiones bajos a campos privados en dos pasos:
  1. Reemplazamos cada guión bajo con un símbolo hash.
  2. Declaramos todos los campos privados al comienzo de la clase.
class Countdown {
  #counter;
  #action;

  constructor(counter, action) {
    this.#counter = counter;
    this.#action = action;
  }
  dec() {
    if (this.#counter < 1) return;
    this.#counter--;
    if (this.#counter === 0) {
      this.#action();
    }
  }
}
// The data is now private:
assert.deepEqual(
  Reflect.ownKeys(new Countdown(5, () => {})),
  []);

Todo el código dentro del cuerpo de una clase puede acceder a todos los campos privados   

Por ejemplo, los métodos de instancia pueden acceder a campos privados estáticos:
class MyClass {
  static #privateStaticField = 1;
  getPrivateFieldOfClass(theClass) {
    return theClass.#privateStaticField;
  }
}
assert.equal(
  new MyClass().getPrivateFieldOfClass(MyClass), 1);
Y los métodos estáticos pueden acceder a campos de instancias privadas:
class MyClass {
  #privateInstanceField = 2;
  static getPrivateFieldOfInstance(theInstance) {
    return theInstance.#privateInstanceField;
  }
}
assert.equal(
  MyClass.getPrivateFieldOfInstance(new MyClass()), 2);

(Avanzado)   

Las secciones restantes cubren aspectos avanzados de los campos privados.

¿Cómo se gestionan los campos privados bajo el capó?  

En la especificación ECMAScript, los campos privados se administran a través de una estructura de datos que se adjunta a los objetos. Es decir, los campos privados se manejan aproximadamente de la siguiente manera.
{ // Begin of class scope

  // Private names
  const __counter = {
    __Description__: 'counter',
    __Kind__: 'field',
  };
  const __action = {
    __Description__: 'action',
    __Kind__: 'field',
  };

  class Object_ {
    // Maps private names to values (a list in the spec).
    __PrivateFieldValues__ = new Map();
  }

  class Countdown extends Object_ {
    static __Fields__ = [__counter, __action];

    constructor(counter, action) {
      super();
      // Setup before constructor
      InitializeInstanceElements(this, Countdown);
      
      // Code inside constructor
      this.__PrivateFieldValues__.set(__counter, counter);
      this.__PrivateFieldValues__.set(__action, action);
    }
    dec() {
      if (this.__PrivateFieldValues__.get(__counter) < 1) return;
      this.__PrivateFieldValues__.set(
        __counter, this.__PrivateFieldValues__.get(__counter) - 1);
      
      if (this.__PrivateFieldValues__.get(__counter) === 0) {
        this.__PrivateFieldValues__.get(__action)();
      }
    }
  }
} // End of class scope

function InitializeInstanceElements(O, constructor) {
  if (constructor.__PrivateBrand__) {
    O.__PrivateBrands__.push(constructor.__PrivateBrand__);
  }
  const fieldRecords = constructor.__Fields__;
  for (const fieldRecord of fieldRecords) {
    O.__PrivateFieldValues__.set(fieldRecord, undefined);
  }
}
Comentarios:
  • En este ejemplo, todo lo que tiene dos guiones bajos son metadatos administrados por el motor de JavaScript y solo accesibles para él.
  • Los nombres privados son claves únicas. Solo son accesibles dentro del cuerpo de la clase.
  • Los valores de campo privado es un diccionario que asigna nombres privados a valores. Cada instancia con campos privados tiene dicho diccionario.
    • Debido a que .__PrivateFieldValues__solo se puede acceder al motor, no necesitamos tomar medidas para protegerlo.
    • (Además, la especificación almacena valores de campos privados en listas. Sin embargo, el uso de Maphizo que el ejemplo sea más fácil de entender).
    • Después de la configuración, si usa un nombre privado que aún no es una clave .__PrivateFieldValues__, el motor arroja un TypeError.
Consecuencias:
  • Solo puede acceder a los datos privados almacenados en .#counter.#actionsi está dentro del cuerpo de la clase Countdown, porque solo tiene acceso a los nombres privados allí.
  • Los campos privados no son accesibles en subclases.

Cada evaluación de una definición de clase produce nuevos nombres privados   

A veces evalúa la misma definición de clase varias veces. Eso es lo que classFactory()hace en el siguiente ejemplo:
const classFactory = () => class {
  static getValue(instance) {
    return instance.#value;
  }
  #value;
  constructor(value) {
    this.#value = value;
  }
};
const Class1 = classFactory();
const Class2 = classFactory();

const inst1 = new Class1(1);
const inst2 = new Class2(2);
El nombre privado #valuese crea recientemente cada vez. Por lo tanto, Class1.getValue()funciona para inst1, pero no para inst2:
assert.equal(Class1.getValue(inst1), 1);
assert.throws(
  () => Class1.getValue(inst2),
  TypeError);

Peligro: uso thispara acceder a campos estáticos privados   

Puede usar thispara acceder a campos estáticos públicos, pero no debe usarlo para acceder a campos estáticos privados.

thisy campos públicos estáticos   

Considere el siguiente código:
class SuperClass {
  static publicData = 1;
  
  static getPublicViaThis() {
    return this.publicData;
  }
}
class SubClass extends SuperClass {
}
Los campos estáticos públicos son propiedades. Si hacemos el método llama:
assert.equal(SuperClass.getPublicViaThis(), 1);
luego thisseñala SuperClassy todo funciona como se esperaba. También podemos invocar a .getPublicViaThis()través de la subclase:
assert.equal(SubClass.getPublicViaThis(), 1);
SubClasshereda .getPublicViaThis()thisseñala SubClassy las cosas siguen funcionando, porque SubClasstambién hereda la propiedad .publicData.
(Como comentario aparte, la configuración .publicDataen este caso crearía una nueva propiedad dentro SubClassque anula no destructivamente la propiedad en SuperClass).

thisy campos estáticos privados   

Considere el siguiente código:
class SuperClass {
  static #privateData = 2;
  static getPrivateDataViaThis() {
    return this.#privateData;
  }
  static getPrivateDataViaClassName() {
    return SuperClass.#privateData;
  }
}
class SubClass extends SuperClass {
}
Invocar a .getPrivateDataViaThis()través de SuperClassobras, porque thisapunta a SuperClass:
assert.equal(SuperClass.getPrivateDataViaThis(), 2);
Sin embargo, invocar .getPrivateDataViaThis()via SubClassno funciona, porque thisahora apunta SubClassSubClassno tiene un campo estático privado .#privateData:
assert.throws(
  () => SubClass.getPrivateDataViaThis(),
  {
    name: 'TypeError',
    message: 'Cannot read private member #privateData from an object whose class did not declare it',
  }
);
La solución es acceder .#privateDatadirectamente, a través de SuperClass:
assert.equal(SubClass.getPrivateDataViaClassName(), 2);

Privacidad de "amigo" y "protegida"   

A veces, queremos que ciertas entidades sean "amigos" de una clase. Dichos amigos deberían tener acceso a los datos privados de la clase. En el siguiente código, la función getCounter()es amiga de la clase CountdownUsamos WeakMaps para hacer que los datos sean privados, lo que permite Countdownque los amigos accedan a esos datos.
const _counter = new WeakMap();
const _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
    let counter = _counter.get(this);
    if (counter < 1) return;
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
      _action.get(this)();
    }
  }
}
function getCounter(countdown) {
  return counter.get(countdown);
}
Es fácil controlar quién tiene acceso a los datos privados: si tienen acceso a _counter_action, tienen acceso a los datos privados. Si colocamos el fragmento de código anterior dentro de un módulo, los datos son privados dentro de todo el módulo.
Para obtener más información sobre esta técnica, consulte la Sección. "Mantener datos privados en WeakMaps" en "JavaScript para programadores impacientes". También funciona para compartir datos privados entre una superclase y subclases (visibilidad "protegida").

FAQ   

¿Por qué el #¿Por qué no declarar campos privados a través de private?  

En principio, los campos privados podrían manejarse de la siguiente manera:
class MyClass {
  private value;
  compare(other) {
    return this.value === other.value;
  }
}
Pero entonces ya no podíamos usar el nombre de la propiedad valuedentro del cuerpo de la clase; siempre se interpretaría como un nombre privado.
Los lenguajes tipados estáticamente como TypeScript tienen más flexibilidad aquí: saben en tiempo de compilación si otheres una instancia de MyClassy luego pueden tratar .valuecomo privados o no.

Lectura adicional   

  • Secta. "Datos privados para clases" en "JavaScript para programadores impacientes".
  • Cap. "WeakMaps ( WeakMap)" en "JavaScript para programadores impacientes".

No hay comentarios.:

Publicar un comentario

Dejanos tu comentario para seguir mejorando!

outbrain

Páginas