Inicio / Artículos

Cierres vs privado vs #: Qué protege cada uno en TypeScript y JavaScript

El acceso privado en TypeScript es una verificación en tiempo de compilación, los campos hash se aplican en tiempo de ejecución, y las clausuras capturan el ámbito léxico. Un análisis comparativo de lo que hace cada uno.

Editor: REACTAPP.TOP7 min de lectura1514 palabras

Fuentes: Closures vs Private in TypeScript: If We Have private and #, Why Do We Still Need Closures?

TypeScript cuenta con private y JavaScript con #; por lo tanto, es lógico preguntarse por qué aún se necesitan cierres para proteger las variables. La respuesta parece obvia hasta que se examina cómo se comporta JavaScript en tiempo de ejecución, y entonces se vuelve más interesante. Los cierres llegan después. Primero, ¿qué hace realmente private en TypeScript?

private en TypeScript

Tomemos una clase con una propiedad privada:

class User {
	private secret: string = "hidden";
}

Ahora cree una instancia y lea la propiedad desde fuera de la clase:

const me = new User();
console.log(me.secret);

La línea clave es esta:

console.log(me.secret);

El compilador, tsc, lo rechaza con un error, por lo que la variable parece estar protegida. El problema es que private es principalmente una característica del sistema de tipos. Cuando TypeScript se convierte en JavaScript, la palabra clave no sobrevive como mecanismo de privacidad en tiempo de ejecución. Este código fuente:

class User {
    private secret: string = "hidden";
}

puede emitirse más o menos así:

class User {
    constructor() {
        this.secret = "hidden";
    }
}

En ese momento, JavaScript no tiene idea de que secret debía ser privado, ya que se trata de una propiedad común. Por lo tanto, lo siguiente:

const me = new User();
console.log(me.secret);

puede imprimir:

hidden

La distinción es importante. Lo que private indica al compilador es que el código que utiliza ese tipo no puede acceder a la propiedad. No le dice al tiempo de ejecución que dicha propiedad debe ser inaccesible, y son promesas muy diferentes.

Qué cambia con #

TypeScript también admite los campos de clase privados nativos de JavaScript:

class User {
    #secret: string = "hidden";
}

Intentar leer el campo desde el exterior ahora falla de manera diferente:

const me = new User();console.log(me.#secret);

Esto no es solo una advertencia del compilador. El campo también es privado en tiempo de ejecución, ya que la sintaxis # crea un verdadero campo de clase privado. Leerlo desde fuera de la clase es ilegal:

console.log(me.#secret);

y leer una propiedad simple del mismo nombre tampoco la alcanza:

console.log(me.secret);

dado que #secret no es una propiedad ordinaria llamada secret. Podría ser tentador imaginarlo como una clave de cadena:

me["#secret"]

Pero eso no funciona. El # no constituye una convención de nomenclatura, ya que el tiempo de ejecución tiene un concepto real de campos privados. Por lo tanto, tenemos dos cosas separadas. La primera está impuesta principalmente por el sistema de tipos:

private secret: string;

y el segundo por la semántica de JavaScript:

#secret: string;

Dónde encajan los cierres

Antes de que existieran los campos de clase privados, los desarrolladores aún necesitaban datos a los que no se pudiera acceder desde el exterior, y una de las herramientas más poderosas que ya tenía el lenguaje era el cierre. He aquí una función fábrica:

function createUser() {
    let secret = "hidden";

Devuelve un objeto que expone un único accesorio:

    return {
        getSecret() {
            return secret;
        }
    };
}const user = createUser();console.log(user.getSecret());

Llamar a ese accesorio imprime:

hidden

Fíjese en lo que falta. No existe un acceso a propiedades de este tipo:

user.secret

El objeto devuelto ni siquiera tiene una propiedad secret. La variable se encuentra en el entorno léxico creado cuando se ejecutó createUser(), y la función getSecret() devuelta recuerda ese entorno. Ese es el núcleo de un cierre: una función mantiene el acceso a las variables del ámbito en el que fue creada, incluso después de que la función exterior haya finalizado. Después de esta llamada:

const user = createUser();

La función externa está terminada. Normalmente se esperaría su variable local:

let secret = "hidden";

desaparecer. Sin embargo, getSecret() aún puede acceder a él, porque se cierra sobre secret, de ahí proviene su nombre.

Estatuto privado sin exponerlo

Las clausuras pueden contener un estado privado sin ninguna variable expuesta. Un contador es un buen ejemplo:

function myCounter() {
    let count = 0;
    return {
        increment() {
            count++;
        },        getCount() {
            return count;
        }
    };
}const counter = myCounter();counter.increment();
counter.increment();console.log(counter.getCount());

Al llamar al método increment dos veces y luego solicitar el conteo, se imprime:

2

mientras que esto no funciona:

console.log(counter.count);

porque count no es una propiedad del contador. Es una variable dentro del entorno léxico capturado. El objeto ofrece el comportamiento, mientras que el estado real se mantiene en otro lugar, una forma de encapsulación basada en el alcance léxico.

¿Es # simplemente mejor?

No necesariamente. Resuelven problemas relacionados y no son la misma funcionalidad. Aquí está la versión de clase:

class User {
    #secret = "hidden";
    getSecret() {
        return this.#secret;
    }
}

En él, #secret representa un estado privado explícito de la clase, propiedad del objeto, y el tiempo de ejecución está al tanto de ello. En la versión con cierre:

function createUser() {
    let secret = "hidden";
    return {
        getSecret() {
            return secret;
        }
    };
}

La variable en absoluto es una propiedad del objeto y se encuentra en el entorno léxico circundante. Los mecanismos son fundamentalmente diferentes. Un campo # indica que este objeto tiene un campo privado, mientras que una cierre indica que esta función puede utilizar el estado del ámbito circundante. Esa diferencia influye en el diseño de las API.

Estatuto compartido entre funciones

Los cierres ofrecen algo más que privacidad: varias funciones pueden compartir un mismo estado. Una cuenta bancaria lo demuestra:

function createBankAccount(initialBalance) {
    let balance = initialBalance;
    return {
        deposit(amount) {
            balance += amount;
        },        withdraw(amount) {
            balance -= amount;
        },        getBalance() {
            return balance;
        }
    };
}

Así se utiliza:

const account = createBankAccount(1000);
account.deposit(500);console.log(account.getBalance());

y el resultado es:

1500

No hay propiedad pública que leer:

account.balance

deposit(), withdraw() y getBalance() acceden a la misma variable local:

balance

Esa es una de las razones por las que los cierres son tan importantes en JavaScript. No son solo un truco para ocultar variables, sino también una forma de mantener el estado entre llamadas.

Corrigiendo una primera suposición

Un resumen tentador es que los cierres proporcionan el mecanismo oculto en tiempo de ejecución, mientras que private y # solo generan alertas en tiempo de compilación. Está cerca, pero no es exacto. private en TypeScript se utiliza principalmente para verificar el acceso en tiempo de compilación, mientras que #private en JavaScript aplica la privacidad en tiempo de ejecución. Los cierres no son ni otra palabra clave ni algo puramente relacionado con el tiempo de compilación. Surgen del ámbito léxico, lo que permite a las funciones mantener acceso a las variables de su entorno inmediato. Esto puede implementar encapsulamiento, pero los cierres son mucho más amplios que la mera privacidad. Una fábrica de funciones muestra un uso sin ningún componente de privacidad:

function createMultiplier(x) {
    return function (y) {
        return x * y;
    };
}
const double = createMultiplier(2);console.log(double(10));

Aquí la función devuelta recuerda a x, y eso es un cierre. El mismo mecanismo permite el estado privado, las fábricas, los callbacks, los manejadores de eventos, la memorización y mucho más.

Un modelo mental de los tres

En TypeScript, private:

class User {
    private secret = "hidden";
}

Considéralo como un sistema de tipos que impide acceder al miembro. En JavaScript, #private:

class User {
    #secret = "hidden";
}

Considérelo como si el tiempo de ejecución tratara el campo como verdaderamente privado. En el caso de un cierre:

function createUser() {
    let secret = "hidden";
    return {
        getSecret() {
            return secret;
        }
    };
}

Considérela como una función que recuerda las variables del ámbito en el que fue creada. Este último punto es el clave. Las clausuras no están obsoletas ahora que existe #, ya que #private es una característica diseñada para campos de clase privados, mientras que las clausuras son una consecuencia del ámbito léxico y de las funciones de primera clase, y sirven para muchos propósitos no relacionados con la privacidad.

Detrás de escena

Queda un punto sutil. Sería incorrecto decir que tanto private como # terminan siendo propiedades ordinarias de JavaScript. Esto aplica a private en TypeScript en la salida habitual, pero no a los campos nativos #private:

class User {
    #secret = "hidden";
}

Esto utiliza una característica en tiempo de ejecución. Dependiendo del destino de compilación, TypeScript puede mantener la sintaxis o reducirla para versiones antiguas de JavaScript, pero las semánticas del lenguaje siguen exigiendo un comportamiento similar al de los campos privados. Por lo tanto, no agrupe private y #private en la misma categoría: se parecen en TypeScript pero se aplican de manera diferente, y los cierres son un mecanismo completamente distinto.

¿Entonces aún se necesitan los cierres?

Sí, y una pregunta mejor que por qué necesitamos cierres a pesar de private y # es qué problema resuelve cada mecanismo. Cada uno satisface una necesidad distinta. private limita el acceso dentro del verificador de tipos, #private hace que el propio tiempo de ejecución aplique las restricciones a los campos ocultos, y los cierres permiten que las funciones sigan utilizando el ámbito en el que fueron creadas. Una vez que esto queda claro, los cierres dejan de parecer una solución temporal para las variables privadas y muestran su verdadero papel como un elemento básico del modelo de programación de JavaScript. Aprender una característica más reciente no hace que un concepto antiguo sea irrelevante; te permite plantear una pregunta más precisa al respecto, es decir, por qué ese mecanismo fue útil en primer lugar. En el caso de los cierres, la respuesta va mucho más allá de simplemente ocultar una variable.

Leer más