Profundizando en Javascript, parte 2: objetos, prototipos, herencia y namespaces

Tras un tiempo de descanso continuamos con la serie “Profundizando en Javascript”. En el post anterior explicaba todas las maneras de definir y usar funciones: con nombre/sin nombre, como expresión/como declaración, anónimas autoejecutables y anidadas (aunque no es obligatorio, es recomendable haber leído antes de continuar).

Hoy vamos a ver como se trabaja con objetos. En Javascript, como en cualquier lenguaje, un objeto no es más que un conjunto de atributos y funciones (sus métodos). A cada objeto podemos añadirle nuevas propiedades y funciones de diferentes maneras que iremos viendo. Sin embargo la herencia, tal y como la conocemos de otros lenguajes como Java, no existe, pues no existen las clases: para crear un nuevo objeto que tenga los mismos atributos que otro, estos deben ser copiados. A esta forma de trabajar se llama herencia basada en prototipos. Esto significa que no hay clases o interfaces como en Java o C++: solo hay objetos que copian (heredan) sus propiedades de otros objetos, llamados prototipos. Así que olvidémonos de la clásica definición de “objeto como instancia de una clase”, porque aquí no hay clases (y por esta razón, cuando hable de clases en Javascript, lo haré entre comillas así: “clases”). Pero vayamos por partes:

Creando objetos

Pare crear un nuevo objeto podemos hacerlo de cualquiera de estas dos formas:

var obj1 = new Object()
var obj2 = {}

La segunda forma se llama “objeto literal”. Es la que usamos para crear lo que en otros lenguajes se llaman diccionarios, mapas o hashtables y que sigue, afortunadamente, la misma sintaxis que JSON.

Para añadir una nueva propiedad, solo tenemos que asignársela:

var obj1 = new Object()
obj1.nombre = "mundo"

alert("hola "+obj1.nombre) // "hola mundo"

Para eliminar una propiedad, usaremos el operador delete:

var obj1 = new Object()
alert(obj1.nombre)   // undefined

obj1.nombre = "mundo"
alert(obj1.nombre)   // "mundo"

delete obj1.nombre

alert(obj1.nombre)   // undefined otra vez

Y para añadir un método, solo hay que añadirle una función como expresión, teniendo siempre en cuenta que para que un método pueda acceder a las propiedades del objeto donde reside, tenemos que usar el prefijo this.

var obj1 = new Object()
obj1.nombre = "mundo"
obj1.saluda = function() { alert("hola "+this.nombre) }
obj1.saluda()

Aquí vemos como es necesario usar this.nombre dentro de la función saluda() para acceder al atributo nombre del objeto.

Otra forma de construir objetos es como literales:

var obj1 = {nombre:"mundo",
            saluda: function() { alert("hola "+this.nombre) }
}
obj1.saluda()

En Javascript podemos acceder a las propiedades de un objeto usando la notación objeto.propiedad; o de manera indexada como un array, con objeto[prop]. En este segundo caso, prop es una variable de tipo String cuyo contenido es el nombre de la propiedad a acceder. Ejemplo:

var obj1 = new Object()
obj1.nombre = "mundo"
obj1.saluda = function() { alert("hola "+this.nombre) }

alert("hola "+obj1.nombre)       // Acceso normal
alert("hola "+obj1[ "nombre" ])  // Acceso indexado con un literal
var prop = "nombre"
alert("hola "+obj1[ prop ])      // Acceso indexado con una variable

obj1.saluda()         // Invocación normal
obj1[ "saluda" ]()    // Acceso indexado con un literal
var func = "saluda"
obj1[ func ]()        // Acceso indexado con una variable

Podemos usar esta segunda forma de acceder a las propiedades de un objeto para listarlas todas, sin importar cuantas tenga ni su nombre:

var obj1 = new Object()
obj1.nombre = "mundo"
obj1.fecha = new Date()
obj1.luckyNumbers = [4,8,15,16,23,42]
obj1.saluda = function() { alert("holas "+this.nombre) }

var name
for (name in obj1) {
	alert(name+" : "+ obj1[name])
}

Prototipos

Pero claro, con esto solo definimos un único objeto, con sus atributos y métodos. Pero, ¿qué pasa si queremos crear una “clase” de la que poder instanciar objetos? Como hemos dicho antes, en Javascript no hay clases, sino prototipos: objetos de los que se copian otros objetos. Así que lo que haremos es crear un objeto del que poder copiar sus propiedades. Para esto Javascript tiene un gran truco: hace que cualquier función pueda ser instanciada como un nuevo objeto usando la palabra clave “new”.

// Una simple función
function Saludator() {
	alert("Saludos factory!!")
}
// Y una simple llamada
Saludator()

A simple vista no es más que una función, ¡y es verdad que lo es! pues podemos llamar a Saludator() como una función de toda la vida. Pero eso no quita que sea también una factoría y que con ella podamos crear nuevos objetos:

// Una función... pero también un prototipo!
function Saludator() {
	alert("Saludos factory!!")
}
// Un nuevo objeto a partir de su "clase" prototipo
var saludo = new Saludator()

Pero claro, con este ejemplo tan sumamente sencillo no se aprecia que hemos creado un nuevo objeto. Así que vamos a añadirle un par de propiedades y métodos (recordemos que solo tenemos que declararlas con el prefijo this).

function Saludator(nom) {
	alert("Saludos factory!!")
	this.nombre = nom+"s"
	this.saluda = function() {
		alert("hola "+this.nombre)
	}
}
var obj1 = new Saludator("mundo")
var obj2 = new Saludator("gente")
obj1.saluda() // "hola mundos"
obj2.saluda() // "hola gentes"

En este ejemplo ya vemos cosas nuevas:
– Que la función Saludator() es el constructor del objeto, y podemos pasarle parámetros que posteriormente podemos usar en los atributos del objeto.
– Que dentro de la función saluda sigue siendo necesario usar this.nombre para acceder al atributo nombre.

Por cierto, podemos crear nuestros prototipos usando funciones como expresiones, y no como declaraciones. El siguiente código es completamente equivalente al anterior:

var Saludator = function(nom) {
	alert("Saludos factory!!")
	this.nombre = nom+"s"
	this.saluda = function() {
		alert("hola "+this.nombre)
	}
}

Es necesario señalar que las funciones que servirán para crear objetos se suelen nombrar con la primera letra en mayúscula. Esto es así para evitar que se confundan con funciones normales o variables.

Métodos privados

Podemos crear métodos “privados” (es decir, métodos que son solo accesibles desde dentro del objeto, nunca desde fuera), creando funciones como declaración, no como expresión. La principal desventaja es que estos tipos de métodos no tienen ligado el “this” con el objeto actual, por lo que no pueden acceder a sus datos internos. Para evitar esto, tenemos que enviar como parámetros los datos del objeto que necesiten. Además, no podemos usar el prefijo this para llamar a estos métodos:

var Saludator = function(nom) {
	this.nombre = nom
	this.saluda = function() {
		alert("hola "+plural(this.nombre))
	}
	function plural(n) {
		return n + "s"
	}
}
var obj1 = new Saludator("mundo")
obj1.saluda()
// obj1.plural()  // fallaría porque plural() es privado

La llamada al método plural() no es accesible desde fuera, por lo que fallaría (por eso está comentada). Observemos también como la llamada a plural() no se hace con this.plural(). Y que para acceder al atributo nombre hemos tenido que pasárselo como parámetro.

Los métodos privados no son dinámicos, en el sentido que no se pueden definir condicionalmente en tiempo de ejecución. Así que el siguiente código no fallará, pero no funcionará correctamente (de hecho, su funcionamiento dependerá del navegador):

// Atención, codigo erróneo a modo de ejemplo, NO USAR!
var Saludator = function(nom, enPlural) {
	this.nombre = nom
	this.saluda = function() {
		alert("hola "+quien(this.nombre))
	}
	// NO HACER ESTO NUNCA!!!!!!!
	if (enPlural) {
		function quien(n) { // Arggg!!
			return n + "s"
		}
	} else {
		function quien(n) { // Ouch!!
			return n
		}
	}
}
var obj1 = new Saludator("mundo", false)
var obj2 = new Saludator("mundo", true)
obj1.saluda() // "hola mundo"
obj2.saluda() // "hola mundo", pero es incorrecto!

En su lugar, definiendo los métodos con funciones como expresiones, si que funciona correctamente, ya que la creación del método no es más que una asignación que se produce dentro de una bifurcación condicional:

var Saludator = function(nom, enPlural) {
	this.nombre = nom
	this.saluda = function() {
		alert("hola "+this.quien())
	}
	if (enPlural) {
		this.quien = function() {
			return this.nombre + "s" // En plural
		}
	} else {
		this.quien = function() {
			return this.nombre  // En singular!!
		}
	}
}
var obj1 = new Saludator("mundo", false)
var obj2 = new Saludator("mundo", true)
obj1.saluda() // "hola mundo"
obj2.saluda() // "hola mundos", correto!

Métodos y atributos estáticos

Para crear un método estático tan solo lo tenemos que asignar a nuestra “clase” una propiedad más (recordemos que no es más que una función):

var Saludator = function(nom, enPlural) {
	this.nombre = nom
	this.enPlural = enPlural
	this.saluda = function() {
		alert("hola "+this.nombre)
	}
}
// Añadimos un método estático como atributo de la "clase" Saludator
Saludator.cloneSaludo = function(saludo) {
	return new Saludator("clonado "+saludo.nombre, saludo.enPlural)
}

var obj1 = new Saludator("mundo", false)
var obj2 = Saludator.cloneSaludo(obj1)
obj2.saluda() // "hola mundo"
obj2.saluda() // "hola clonado mundo"

Por supuesto, podemos añadir también atributos estáticos. Todas las propiedades estáticas (atributos o métodos) son siempre públicas y necesitan SIEMPRE el prefijo con el nombre de la “clase” delante siempre para acceder a ellas.

En este ejemplo, se usa la propiedad estática count de la clase Saludator para llevar la cuenta del número de objetos que se han creado:

var Saludator = function(nom) {
	// Se inicializa count a 0, si no existe
	Saludator.count = Saludator.count?Saludator.count:0
	Saludator.count ++
	this.nombre = nom
	this.saluda = function() {
		alert("hola "+this.nombre)
	}

}
Saludator.cloneSaludo = function(saludo) {
	return new Saludator("clonado "+saludo.nombre)
}

var obj1 = new Saludator("mundo", false)
var obj2 = Saludator.cloneSaludo(obj1)
obj2.saluda()

alert(Saludator.count) // "2", pues se han creado 2 objetos

Herencia

Como hemos dicho antes, la herencia basada en clases no existe en Javascript, sino que es herencia basada en prototipos. Esto significa que no hay una jerarquía de clases real (con su extends, acceso super, etc.), sino que cada “clase” tiene un objeto de referencia (como si fuera su clase padre) donde consultar atributos y métodos en caso de que nuestra clase no los encuentre. Podemos intentar simular una jerarquía (ojo, simular) usando estos objetos de referencia, llamados prototipos, con una propiedad especial que tienen todas las “clases” llamada prototype. Esta propiedad es asignable y su labor consiste en guardar un objeto. Veámoslo con un ejemplo:

var Saludator = function(nom) {
	this.nombre = nom
	this.saluda = function() { 
		alert("hola "+this.nombre) 
	}
}
// Añade un objeto al prototipo
Saludator.prototype = {
	apellido: "cruel",
	despide: function() { 
		alert("adios "+this.nombre+" "+this.apellido)
	}
}
var obj1 = new Saludator("mundo")
obj1.despide() // "adios mundo cruel"

Con esto lo que hacemos es asignar a la variable estática prototype en la clase Saludator un simple objeto con un atributo apellido y un método despide. Cuando invocamos a obj1.despide(), Javascript primero consulta en la propia clase Saludator y busca ese método; al no encontrarlo, consulta en el objeto prototype, donde si lo encuentra, por lo que lo ejecuta. Dentro de este método, se accede a this.nombre y this.apellido y sucede lo mismo: primero Javascript mira en la clase Saludator, donde solo encuentra el atributo nombre. Sin embargo, para encontrar el atributo apellido debe consultar en el objeto prototype.

Todas las clases tienen por defecto un objeto prototype vacío, como si al crearlas se hiciera prototype = {}. Por esta razón, podemos acceder a esta variable y asignarle los atributos de uno en uno. El siguiente ejemplo es completamente equivalente al anterior:

var Saludator = function(nom) {
	this.nombre = nom
}
// Añade nuevas propiedades al prototipo
Saludator.prototype.apellido = "cruel"
Saludator.prototype.despide = function() {
	alert("adios "+this.nombre+" "+this.apellido)
}

var obj1 = new Saludator("mundo")
obj1.despide() // "adios mundo cruel"

El problema de la herencia basada en prototipos es que la “clase padre” no es una clase, sino un objeto que se crea una única vez y que sirve de referencia para todas las instancias de la clase. Además, en una herencia basada en clases como la de Java, los constructores de las clases padre se heredan automáticamente (aunque no se invocan al instanciar nuevos objetos), pero en una de prototipos no. Así que para poder heredar e invocar constructores en Javascript tenemos que hacerlo a mano. Primero debemos crear una referencia al constructor de la clase padre y después llamarlo explícitamente:

var Saludator = function(nom) {
	this.nombre = nom
	this.saluda = function() { 
		alert("hola "+this.nombre) 
	}
}
var SubSaludator = function(nom, ape) {
	// Primero se añade una referencia al constructor padre,
	// y después se llama explícitamente.
	this.superConstructor = Saludator
	this.superConstructor(nom)
	
	this.apellido = ape
	this.despide = function() { 
		alert("adios "+this.nombre+" "+this.apellido)
	}
}
SubSaludator.prototype = new Saludator()

var obj1 = new SubSaludator("mundo", "cruel")
var obj2 = new SubSaludator("gente", "cruel")

obj1.saluda()  // "hola mundo"
obj2.saluda()  // "hola gente"
obj1.despide() // "adios mundo cruel"
obj2.despide() // "adios gente cruel"

Finalmente decir que podemos acceder al atributo prototype en todas clases, incluso en las clases del sistema, como String o Date. El siguiente código añade un nuevo método capitalize() a la clase de sistema String:

String.prototype.capitalize = function() {
	return this.charAt(0).toUpperCase() + this.substr(1).toLowerCase();
}

alert("hola".capitalize())

Namespaces

Una perlita para acabar. En algunos frameworks vemos como las “clases” tienen paquetes o espacio de nombres. Para empezar decir que ni son namespaces, ni packages ni nada, son solo objetos anidados unos dentro de otros. Y para acabar decir que es una muy buena practica que nosotros agrupemos nuestras funciones y objetos usando nuestro propio “espacio de nombres”.

Veámoslo con un ejemplo: es habitual tener varias funciones sueltas que nos ayuden (para manipular el Dom, validar un dato o lo que sea) junto con variables sueltas para apoyarnos así:

var dirty = false
function clickCancel() {
	var save = true
	if (dirty) {
		save = confirm("salvar antes de salir?")
	}
	if (save) clickSave()
}

function clickSave() {
	document.getElementById("myForm").submit()
}

Aquí tenemos una variable publica llamada dirty y dos métodos, también públicos, llamados clickCancel y clickSave. Pero sería mucho mejor usar nuestro propio objeto para encapsularlos:

var App = {
	dirty: false,
	clickCancel: function() {
		var save = true
		if (this.dirty) {
			save = confirm("salvar antes de salir?")
		}
		if (save) this.clickSave()
	},
	clickSave: function() {
		document.getElementById("myForm").submit()
	}
}

Ahora usaremos App.dirty, App.clickCancel() y App.clickSave(), consumiendo solo una variable pública en el ámbito global llamada App, y no tres como hacíamos antes. Si tenemos en cuenta que, generalmente, solemos crear decenas de funciones y variables públicas, está claro que la posibilidad de que usemos el mismo nombre de función o variable que ya exista en otro sitio disminuye considerablemente.

Y podemos anidar varios objetos, unos dentro de otros, hasta construir nuestro propio espacio de nombres:

var app = {util:{}}

app.util.forms = {
	dirty: false,
	clickCancel: function() {
		var save = true
		if (this.dirty) {
			save = confirm("salvar antes de salir?")
		}
		if (save) this.clickSave()
	},
	clickSave: function() {
		document.getElementById("myForm").submit()
	}
}

Ahora tenemos nuestro flamante espacio de nombres app.util.forms.dirty y podemos usar las funciones como app.util.forms.clickCancel() y app.util.forms.clickSave()

Y nada más por hoy. En el próximo post cerraremos la serie de Javascript hablando de scopes (ámbitos de visibilidad), hoisting y closures que es cuando la cosa se empieza a poner interesante de verdad. Y veremos también algunos ejemplos un poco más complejos, ¡hasta entonces!

Más información: