Ver índice de contenidos del libro

3.2. Clases

Los objetos que se han visto hasta ahora son una simple colección de propiedades y métodos que se definen para cada objeto individual. Sin embargo, en la programación orientada a objetos, el concepto fundamental es el de clase.

La forma habitual de trabajo consiste en definir clases a partir de las cuales se crean los objetos con los que trabajan las aplicaciones. Sin embargo, JavaScript no permite crear clases similares a las de lenguajes como Java o C++. De hecho, la palabra class sólo está reservada para su uso en futuras versiones de JavaScript.

A pesar de estas limitaciones, es posible utilizar en JavaScript unos elementos parecidos a las clases y que se denominan pseudoclases. Los conceptos que se utilizan para simular las clases son las funciones constructoras y el prototype de los objetos.

3.2.1. Funciones constructoras

Al contrario que en los lenguajes orientados a objetos, en JavaScript no existe el concepto de constructor. Por lo tanto, al definir una clase no se incluyen uno o varios constructores. En realidad, JavaScript emula el funcionamiento de los constructores mediante el uso de funciones.

En el siguiente ejemplo, se crea un objeto genérico y un objeto de tipo array:

var elObjeto = new Object();
var elArray = new Array(5);

En los dos casos, se utiliza la palabra reservada new y el nombre del tipo de objeto que se quiere crear. En realidad, ese nombre es el nombre de una función que se ejecuta para crear el nuevo objeto. Además, como se trata de funciones, es posible incluir parámetros en la creación del objeto.

JavaScript utiliza funciones para simular los constructores de objetos, por lo que estas funciones se denominan "funciones constructoras". El siguiente ejemplo crea una función llamada Factura que se utiliza para crear objetos que representan una factura.

function Factura(idFactura, idCliente) {
  this.idFactura = idFactura;
  this.idCliente = idCliente;
}

Cuando se crea un objeto es habitual pasar al constructor de la clase una serie de valores para inicializar algunas propiedades. Este concepto también se utiliza en JavaSript, aunque su realización es diferente. En este caso, la función constructora inicializa las propiedades de cada objeto mediante el uso de la palabra reservada this.

La función constructora puede definir todos los parámetros que necesita para construir los nuevos objetos y posteriormente utilizar esos parámetros para la inicialización de las propiedades. En el caso anterior, la factura se inicializa mediante el identificador de factura y el identificador de cliente.

Después de definir la función anterior, es posible crear un objeto de tipo Factura y simular el funcionamiento de un constructor:

var laFactura = new Factura(3, 7);

Así, el objeto laFactura es de tipo Factura, con todas sus propiedades y métodos y se puede acceder a ellos utilizando la notación de puntos habitual:

alert("cliente = " + laFactura.idCliente + ", factura = " + laFactura.idFactura);

Normalmente, las funciones constructoras no devuelven ningún valor y se limitan a definir las propiedades y los métodos del nuevo objeto.

3.2.2. Prototype

Las funciones constructoras no solamente pueden establecer las propiedades del objeto, sino que también pueden definir sus métodos. Siguiendo con el ejemplo anterior, se puede crear un objeto completo llamado Factura con sus propiedades y métodos:

function Factura(idFactura, idCliente) {
  this.idFactura = idFactura;
  this.idCliente = idCliente;
 
  this.muestraCliente = function() {
    alert(this.idCliente);
  }
 
  this.muestraId = function() {
    alert(this.idFactura);
  }
}

Una vez definida la pseudoclase mediante la función constructora, ya es posible crear objetos de ese tipo. En el siguiente ejemplo se crean dos objetos diferentes y se emplean sus métodos:

var laFactura = new Factura(3, 7);
laFactura.muestraCliente();
var otraFactura = new Factura(5, 4);
otraFactura.muestraId();

Incluir los métodos de los objetos como funciones dentro de la propia función constructora, es una técnica que funciona correctamente pero que tiene un gran inconveniente que la hace poco aconsejable.

En el ejemplo anterior, las funciones muestraCliente() y muestraId() se crean de nuevo por cada objeto creado. En efecto, con esta técnica, cada vez que se instancia un objeto, se definen tantas nuevas funciones como métodos incluya la función constructora. La penalización en el rendimiento y el consumo excesivo de recursos de esta técnica puede suponer un inconveniente en las aplicaciones profesionales realizadas con JavaScript.

Afortunadamente, JavaScript incluye una propiedad que no está presente en otros lenguajes de programación y que soluciona este inconveniente. La propiedad se conoce con el nombre de prototype y es una de las características más poderosas de JavaScript.

Todos los objetos de JavaScript incluyen una referencia interna a otro objeto llamado prototype o "prototipo". Cualquier propiedad o método que contenga el objeto prototipo, está presente de forma automática en el objeto original.

Realizando un símil con los lenguajes orientados a objetos, es como si cualquier objeto de JavaScript heredara de forma automática todas las propiedades y métodos de otro objeto llamado prototype. Cada tipo de objeto diferente hereda de un objeto prototype diferente.

En cierto modo, se puede decir que el prototype es el molde con el que se fabrica cada objeto de ese tipo. Si se modifica el molde o se le añaden nuevas características, todos los objetos fabricados con ese molde tendrán esas características.

Normalmente los métodos no varían de un objeto a otro del mismo tipo, por lo que se puede evitar el problema de rendimiento comentado anteriormente añadiendo los métodos al prototipo a partir del cual se crean los objetos.

Si se considera de nuevo la clase Factura del ejemplo anterior:

function Factura(idFactura, idCliente) {
  this.idFactura = idFactura;
  this.idCliente = idCliente;
 
  this.muestraCliente = function() {
    alert(this.idCliente);
  }
 
  this.muestraId = function() {
    alert(this.idFactura);
  }
}

La clase anterior que incluye los métodos en la función constructora, se puede reescribir utilizando el objeto prototype:

function Factura(idFactura, idCliente) {
  this.idFactura = idFactura;
  this.idCliente = idCliente;
}
 
Factura.prototype.muestraCliente = function() {
  alert(this.idCliente);
}
 
Factura.prototype.muestraId = function() {
  alert(this.idFactura);
}

Para incluir un método en el prototipo de un objeto, se utiliza la propiedad prototype del objeto. En el ejemplo anterior, se han añadido los dos métodos del objeto en su prototipo. De esta forma, todos los objetos creados con esta función constructora incluyen por defecto estos dos métodos. Además, no se crean dos nuevas funciones por cada objeto, sino que se definen únicamente dos funciones para todos los objetos creados.

Evidentemente, en el prototype de un objeto sólo se deben añadir aquellos elementos comunes para todos los objetos. Normalmente se añaden los métodos y las constantes (propiedades cuyo valor no varía durante la ejecución de la aplicación). Las propiedades del objeto permanecen en la función constructora para que cada objeto diferente pueda tener un valor distinto en esas propiedades.

El mayor inconveniente de la propiedad prototype es que se pueden reescribir propiedades y métodos de forma accidental. Si se considera el siguiente ejemplo:

Factura.prototype.iva = 16;
var laFactura = new Factura(3, 7);   // laFactura.iva = 16
 
Factura.prototype.iva = 7;
var otraFactura = new Factura(5, 4);
// Ahora, laFactura.iva = otraFactura.iva = 7

El primer objeto creado de tipo Factura dispone de una propiedad llamada iva cuyo valor es 16. Más adelante, se modifica el prototipo del objeto Factura durante la ejecución del programa y se establece un nuevo valor en la propiedad iva.

De esta forma, la propiedad iva del segundo objeto creado vale 7. Además, el valor de la propiedad iva del primer objeto ha cambiado y ahora vale 7 y no 16. Aunque la modificación del prototipo en tiempo de ejecución no suele ser una operación que se realice habitualmente, sí que es posible modificarlo de forma accidental.

Ejercicio 2

Modificar el ejercicio anterior del objeto Factura para crear una pseudoclase llamada Factura y que permita crear objetos de ese tipo. Se deben utilizar las funciones constructoras y la propiedad prototype.

Para instanciar la clase, se debe utilizar la instrucción Factura(cliente, elementos), donde cliente también es una pseudoclase que guarda los datos del cliente y elementos es un array simple que contiene las pseudoclases de todos los elementos que forman la factura.

Ver solución

Una de las posibilidades más interesantes de la propiedad prototype es que también permite añadir y/o modificar las propiedades y métodos de los objetos predefinidos por JavaScript. Por lo tanto, es posible redefinir el comportamiento habitual de algunos métodos de los objetos nativos de JavaScript. Además, se pueden añadir propiedades o métodos completamente nuevos.

Si por ejemplo se considera la clase Array, esta no dispone de un método que indique la posición de un elemento dentro de un array (como la función indexOf() de Java). Modificando el prototipo con el que se construyen los objetos de tipo Array, es posible añadir esta funcionalidad:

Array.prototype.indexOf = function(objeto) {
  var resultado = -1;
  for(var i=0; i<this.length; i++) {
    if(this[i] == objeto) {
      resultado = i;
      break;
    }
  }
  return resultado;
}

El código anterior permite que todos los arrays de JavaScript dispongan de un método llamado indexOf que devuelve el índice de la primera posición de un elemento dentro del array o -1 si el elemento no se encuentra en el array, tal y como sucede en otros lenguajes de programación.

El funcionamiento del método anterior consiste en recorrer el array actual (obteniendo su número de elementos mediante this.length) y comparar cada elemento del array con el elemento que se quiere encontrar. Cuando se encuentra por primera vez el elemento, se devuelve su posición dentro del array. En otro caso, se devuelve -1.

Como se verá más adelante, existen librerías de JavaScript formadas por un conjunto de utilidades que facilitan la programación de las aplicaciones. Una de las características habituales de estas librerías es el uso de la propiedad prototype para mejorar las funcionalidades básicas de JavaScript.

A continuación se muestra el ejemplo de una librería llamada Prototype que utiliza la propiedad prototype para ampliar las funcionalidades de los arrays de JavaScript:

Object.extend(Array.prototype, {
  _each: function(iterator) {
    for (var i = 0; i < this.length; i++)
      iterator(this[i]);
  },
  clear: function() {
    this.length = 0;
    return this;
  },
  first: function() {
    return this[0];
  },
  last: function() {
    return this[this.length - 1];
  },
  compact: function() {
    return this.select(function(value) {
      return value != undefined || value != null;
    });
  },
  flatten: function() {
    return this.inject([], function(array, value) {
      return array.concat(value.constructor == Array ?
        value.flatten() : [value]);
    });
  },
  without: function() {
    var values = $A(arguments);
    return this.select(function(value) {
      return !values.include(value);
    });
  },
  indexOf: function(object) {
    for (var i = 0; i < this.length; i++)
      if (this[i] == object) return i;
    return -1;
  },
  reverse: function(inline) {
    return (inline !== false ? this : this.toArray())._reverse();
  },
  shift: function() {
    var result = this[0];
    for (var i = 0; i < this.length - 1; i++)
      this[i] = this[i + 1];
    this.length--;
    return result;
  },
  inspect: function() {
    return '[' + this.map(Object.inspect).join(', ') + ']';
  }
});

El código anterior añade a la clase Array de JavaScript varias utilidades que no disponen los arrays por defecto: obtener (sin extraerlo) el primer o último elemento del array, filtrar sus valores, determinar la posición de un elemento, borrar el array, etc.

Ejercicio 3

Extender el objeto Array para que permita añadir nuevos elementos al final del array:

var array1 = [0, 1, 2];
array1.anadir(3);
// array1 = [0, 1, 2, 3]

Incluir la opción de controlar si se permiten elementos duplicados o no:

var array1 = [0, 1, 2];
 
array1.anadir(2);
// array1 = [0, 1, 2, 2]
 
array1.anadir(2, false);
// array1 = [0, 1, 2]

Ver solución

Cualquier clase nativa de JavaScript puede ser modificada mediante la propiedad prototype, incluso la clase Object. Se puede modificar por ejemplo la clase String para añadir un método que convierta una cadena en un array:

String.prototype.toArray = function() {
  return this.split('');
}

La función split() divide una cadena de texto según el separador indicado. Si no se indica ningún separador, se divide carácter a carácter. De este modo, la función anterior devuelve un array en el que cada elemento es una letra de la cadena de texto original.

Una función muy común en otros lenguajes de programación y que no dispone JavaScript es la función trim(), que elimina el espacio en blanco que pueda existir al principio y al final de una cadena de texto:

String.prototype.trim = function() {
  return this.replace(/^\s*|\s*$/g, '');
}
 
var cadena = "   prueba   de     cadena      ";
cadena.trim();
// Ahora cadena = "prueba   de     cadena"

La función trim() añadida al prototipo de la clase String hace uso de las expresiones regulares para detectar todos los espacios en blanco que puedan existir tanto al principio como al final de la cadena y se sustituyen por una cadena vacía, es decir, se eliminan.

Con este método, es posible definir las funciones asociadas rtrim() y ltrim() que eliminan los espacios en blanco a la derecha (final) de la cadena y a su izquierda (principio).

String.prototype.rtrim = function() {
  return this.replace(/\s*$/g, '');
}
String.prototype.ltrim = function() {
  return this.replace(/^\s*/g, '');
}
String.prototype.trim = function() {
  return this.ltrim().rtrim();
}

Otra función muy útil para las cadenas de texto es la de eliminar todas las etiquetas HTML que pueda contener. Cualquier aplicación en la que el usuario pueda introducir información, debe tener especial cuidado con los datos introducidos por el usuario. Mediante JavaScript se puede modificar la clase String para incluir una utilidad que elimine cualquier etiqueta de código HTML de la cadena de texto:

String.prototype.stripTags = function() {
  return this.replace(/<\/?[^>]+>/gi, '');
}
 
var cadena = '<html><head><meta content="text/html; charset=UTF-8" http-equiv="content-type"></head><body><p>Parrafo de prueba</p></body></html>';
cadena.stripTags();
// Ahora cadena = "Parrafo de prueba"

El ejemplo anterior también hace uso de expresiones regulares complejas para eliminar cualquier trozo de texto que sea similar a una etiqueta HTML, por lo que se buscan patrones como <...> y </...>.

Ejercicio 4

Extender la clase String para que permita truncar una cadena de texto a un tamaño indicado como parámetro:

var cadena = "hola mundo";
cadena2 = cadena.truncar(6); // cadena2 = "hola m"

Modificar la función anterior para que permita definir el texto que indica que la cadena se ha truncado:

var cadena = "hola mundo";
cadena2 = cadena.truncar(6, '...'); // cadena2 = "hol..."

Ver solución

Ejercicio 5

Añadir a la clase Array un método llamado sin() que permita filtrar los elementos del array original y obtenga un nuevo array con todos los valores diferentes al indicado:

var array1 = [1, 2, 3, 4, 5];
var filtrado = array1.sin(4);  // filtrado = [1, 2, 3, 5]

Ver solución

3.2.3. Herencia y ámbito (scope)

Los lenguajes orientados a objetos disponen, entre otros, de los conceptos de herencia entre clases y de ámbito scope) de sus métodos y propiedades (public, private, protected).

Sin embargo, JavaScript no dispone de forma nativa ni de herencia ni de ámbitos. Si se requieren ambos mecanismos, la única opción es simular su funcionamiento mediante clases, funciones y métodos desarrollados a medida.

Algunas técnicas simulan el uso de propiedades privadas prefijando su nombre con un guión bajo, para distinguirlas del resto de propiedades públicas. Para simular la herencia de clases, algunas librerías como Prototype añaden un método a la clase Object llamado extend() y que copia las propiedades de una clase origen en otra clase destino:

Object.extend = function(destination, source) {
  for (var property in source) {
    destination[property] = source[property];
  }
  return destination;
}