Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

JavaScript: Variables y funciones privadas

keep-calm-and-learn-javascriptUna de las cosas que echa en falta la mayoría de la gente cuando empieza a desarrollar en JavaScript es la visibilidad de las funciones, propiedades o variables. En efecto JavaScript no tiene modificadores de visibilidad y todo es, por defecto, accesible (vamos, lo que en la mayoría de lenguajes se conoce como público). Pero que no exista un modificador para especificar un modificador de visibilidad no significa que no puedan declararse miembros privados. Se puede y de hecho es (por supuesto) una buena práctica. Veamos cómo podríamos hacerlo de distintas maneras.

Opción 1 – No hacer nada y establecer nomenclatura

Vale, esa no es una opción real pero es muy utilizada hoy en día. Mucha gente usa el guión bajo (o a veces el doble guión bajo) para indicar que algo es privado y que no debe ser modificado directamente. Por supuesto, eso es una convención, nada impide realmente modificar el valor del miembro afectado:

var Beer = function(name) {
    this.__name = name || "unnamed";
    this.setName = function(newname) {
        if (newname) {
            this.__name = newname;
        }
    }
    this.getName = function() {
        return this.__name;
    }
}

var beer = new Beer("Punk IPA");
console.log(beer.getName());		// Imprime Punk IPA
beer.setName();
console.log(beer.getName());		// Imprime Punk IPA
beer.__name = undefined;
console.log(beer.getName());		// Imprime undefined

Se puede ver como, a pesar de que la función setName evita que se pueda establecer el nombre a undefined, nada impide que se pueda acceder internamente a la variable __name. Aunque, como vamos a ver ahora, existen mecanismos para crear realmente variables (y funciones) privadas. Si no quieres hacerlo, al menos usa esa convención.

Opción 2 – Retornar solo la parte pública

Por defecto si no devolvemos nada, las funciones en JavaScript devuelven undefined, excepto si son llamadas con new que entonces devuelven el propio objeto que se está creando (y que se referencia por this). Pero nada impide devolver otro objeto que contenga tan solo los miembros públicos:

var Beer = function(name) {
    var __name = name || "unnamed";
    var setName = function(newname) {
        if (newname) {
            __name = newname;
        }
    }
    var getName = function() {
        return __name;
    }

    return {
        getName: getName,
        setName: setName
    }
}

var beer = new Beer("Punk IPA");
var beer2 = new Beer("Hoegaarden");
beer2.__name = "Sang de rabo de drac";
beer2.setName("Devil's IPA");
console.log(beer.getName());    // Imprime Punk IPA
console.log(beer2.getName());   // Imprime Devil's IPA
console.log(beer.__name);       // Imprime undefined
console.log(beer2.__name);      // Imprime Sang de rabo de drac

Observa ahora que dentro de la función Beer no usamos this para nada. Creamos tres miembros (__name, setName y getName) y luego devolvemos un objeto que contiene solo los dos miembros públicos. Fíjate que si establecemos desde fuera el valor de __name, lo que en realidad estamos haciendo es crear un nuevo miembro __name dentro del objeto, miembro que nada tiene que ver con el __name que está dentro de la función Beer. Así, puedes observar como beer2.getName() devuelve “Devil’s IPA” (que es el valor establecido con setName) y no “Sang de rabo de drac” que es el valor del miembro __name creado por nosotros. A todos los efectos la variable __name es inaccesible desde fuera del código de la función Beer (observa también que beer.__name vale undefined, ya que a todos los efectos el objeto beer no tiene ningún miembro __name).

Ah, por cierto: si usas esa técnica (devolver solo la parte pública) no es necesario que los objetos se creen con new. En este caso beer = new Beer(); es equivalente a beer = Beer(); ya que new lo único que hace es que en la función llamada (Beer) el valor de this referencie al nuevo objeto que se está creando y que se devuelva this por defecto (y no undefined). Pero si, como es este caso, no usas this dentro de la función y devuelves algo de forma explícita, new pasa a ser totalmente opcional. Sí: JavaScript es muy distinto a Java o a C#.

Opción 3 – Funciones adicionales fuera de this

Otro mecanismo adicional es definir funciones o variables y no colocarlos como miembros de this:

var Beer = function(name) {
    var __name = name || "unnamed";
    this.setName = function(newname) {
        if (newname) {
            __name = newname;
        }
    }
    this.getName = function() {
        return __name;
    }
}

var beer = new Beer("Punk IPA");
var beer2 = new Beer("Hoegaarden");
console.log(beer);
beer2.__name = "Sang de rabo de drac";
beer2.setName("Devil's IPA");
console.log(beer.getName());    // Imprime Punk IPA
console.log(beer2.getName());   // Imprime Devil's IPA
console.log(beer.__name);       // Imprime undefined
console.log(beer2.__name);      // Imprime Sang de rabo de drac

Observa ahora que tanto setName como getName se colocan como miembros de this, pero __name no. De nuevo __name actúa como “miembro privado”. En este caso, dado que estamos usando this dentro de Beer, los objetos deben ser creados mediante new.

Si usas esa técnica puedes tener dificultades si quieres acceder desde una función privada a un miembro público. Imagina el siguiente código:

var Beer = function(name) {
    var __name = name || "unnamed";
    this.setName = function(newname) {
        if (newname) {
            __name = newname;
        }
    }
    this.getName = function() {
        return __name;
    }

    this.dump = function() {
        console.log("Beer is " + prettyName());
    }

    var prettyName = function() {
        return this.getName().toUperCase();
    }
}

var beer = new Beer("Punk IPA");
beer.dump();

Al ejecutarlo, se podría pensar que debería aparecer por pantalla “Beer is PUNK IPA”, puesto que el método dump llama al método prettyName para obtener el nombre de la cerveza en mayúsculas. Pero ese código da un error de “undefined is not a function” y ese error se produce en la llamada a this.getName() dentro del código de prettyName. Es decir, cuando desde una función privada llamamos a un método público tenemos un error. Eso es debido a que el método privado prettyName no forma, realmente, parte del objeto (nunca lo hemos asignado a this) y por lo tanto, dentro de este método el valor de this no es el del objeto, si no que es el del contexto global (window en un navegador).

¿Cuál es la solución a este problema? Pues hay dos. La más extendida pero menos elegante (aunque como digo muy usada) y la menos extendida pero más elegante. Veamos ambas.

La primera consiste en guardarse el valor de this dentro de otra variable y usar dicha variable para acceder al objeto. Comúnmente se llama self o that a esa variable, aunque eso es simplemente una convención:

var Beer = function(name) {
    var __name = name || "unnamed";
    var self = this;
    this.setName = function(newname) {
        if (newname) {
            __name = newname;
        }
    }
    this.getName = function() {
        return __name;
    }

    this.dump = function() {
        console.log("Beer is " + prettyName());
    }

    var prettyName = function() {
        return self.getName().toUpperCase();
    }
}

var beer = new Beer("Punk IPA");
beer.dump();

Este código es muy similar al anterior. Observa tan solo como guardamos el valor de this en la variable self y usamos esta variable para acceder al propio objeto desde aquellos métodos que están fuera de él (como prettyName).

El segundo método es más elegante porque no requiere usar ninguna variable extra: consiste en usar call para forzar que dentro de prettyName la variable this tome el valor que queremos; esto es, el del propio objeto:

var Beer = function(name) {
    var __name = name || "unnamed";
    this.setName = function(newname) {
        if (newname) {
            __name = newname;
        }
    }
    this.getName = function() {
        return __name;
    }

    this.dump = function() {
        console.log("Beer is " + prettyName.call(this));
    }

    var prettyName = function() {
        return this.getName().toUpperCase();
    }
}

var beer = new Beer("Punk IPA");
beer.dump();

Fíjate ahora en que dentro de prettyName usamos this para acceder al objeto. ¿Por qué ahora this toma el valor del propio objeto dentro de prettyName cuando antes tenía el valor del contexto global? Pues porque cuando llamamos a prettyName usamos call. Observa que dentro de dump llamamos a prettyName usando prettyName.call(this). El método call es un método especial en JavaScript que permite redefinir el valor que tendrá this dentro del método que se llama: el valor de this dentro de la función llamada será el del primer parámetro que se pase a call. En este caso dentro de dump el valor de this es el del propio objeto, por lo que cuando usamos prettyName.call(this) estamos llamando a prettyName e indicando que dentro de prettyName el valor de this sea el mismo que el de this dentro de dump (es decir, el propio objeto).

Se puede ver como, a pesar de no tener un soporte directo, es posible y sencillo definir funciones y variables privadas en JavaScript y que no hay realmente excusa para no hacerlo. Como siempre me gusta recalcar, JavaScript es un lenguaje muy distinto a Java o a C# por lo que hemos de pensar de forma diferente para poder sacarle el máximo provecho.

Eduard Tomás Eduard es ingeniero informático, atesora muchos años de experiencia como desarrollador y ha sido galardonado como MVP por Microsoft en diez ocasiones. Colabora con la comunidad impartiendo charlas en formato webcast y en eventos de distintos grupos de usuarios. Es Certified Kubernetes Application Developer. Ver todos los posts de Eduard Tomás
Archivado en: Desarrollo Web

Boletín campusMVP.es

Solo cosas útiles. Una vez al mes.

🚀 Únete a miles de desarrolladores

DATE DE ALTA

x No me interesa | x Ya soy suscriptor

La mejor formación online para desarrolladores como tú

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.