Programación Orientada a Objetos (POO) en JavaScript

Descripción de POO: como crear clases, herencias, polimorfismo, etc. en JavaScript

  • Clase es definicion como tiene que ser.
  • Objeto es una instancia de la clase con valores aplicados y particulares.
  • En JS no existen interfaces.

Los tres pilares básicos de la Programación Orientada a Objetos son:
A. Encapsulación.
B. Herencia.
C. Polimorfismo.

2.1. Redefiniendo objetos genericos:
Crear objeto:

var marcianito = new Object(); // Creamos objeto
marcianito1.name = “invasor del espacio #1”; // Se crea nueva propiedad y se asigna valor a ella
marcianito1.color = “Azul”;
marcianito1.x = 100;
marcianito1.y = 20;
marcianito1.disparos = 30;
marcianito1.disparos++; // Incrementamos valor de propiedad

Crear metodos:
marcianito1.disparar = function () {
this.disparos–;
alert(this.name + ” ha disparado.”);
}

Uso de metodos:
marcianito1.disparar();

2.2. Sintaxis JSON para objetos (JavaScript Object Notation). No es muy util para hacer POO.

var marcianito1 = {
name : “invasor del espacio #1”, // Uso de “:” para asignacion de valor
color : “Azul”, // Uso de “,” despues de cada propiedad si despues hay otra
x : 100,
y : 20,
disparos : 30,
disparar : function () {
this.disparos–;
alert(this.name + ” ha disparado.”);
}
}

marcianito1.disparar(); // Funciona exactamente igual.

2.3. Patrón factoría

Creando objetos usando una funcion.

function crearMarcianito(elNombre, elColor, posX, posY, disparosIniciales) {
return {
name : elNombre,
color : elColor,
x : posX,
y : posY,
disparos : disparosIniciales,
disparar : function () {
this.disparos–;
alert(this.name + ” ha disparado.”);
}
};
}

var marcianito1 = crearMarcianito(“Invasor #1”, “Azul”, 0, 0, 30);
var marcianito2 = crearMarcianito(“Invasor #2”, “Verde”, 100, 300, 50);
alert(marcianito1.name);
alert(marcianito2.name);

  1. “new” para crear objetos. Llama al constructor que contiene todas las clases.
    “instanceof” para identificar la clase de objeto.
    3.1. Constructores reales de objetos:
    “this.” para crear propiedades de propio objeto. Ej: function Marcianito(elNombre,…){ this.name = elNombre; }
    “new”. Ej.: var item = Marcianito(“Invasor #1”,…);
    3.2. Fundamentos de la variable especial this.
  • Depende desde donde se llama al “this.” le da un valor u otro.
  • “this.” en constructor (en llamada “new”) – acceso a propio objeto para crear propiedades. JS al llamar a funcion con “new”, pasa al constructor nuevo objeto vacio.
  • Si no ponemos “new”, “this.” no sera nuevo objeto y no se asignaran propiedades. No devolvera objeto esta funcion. Dentro de funcion “this.” representa que es la ventana del navegador. Es objeto global del ejecutor del codigo, en navegadores es ventana.
  • Si no ponemos “this.”, ni “var”, se definen propiedades globales como si se llaman con “this.”.
  • Objetos se escriben con PascaleCase (Primera letra mayuscula).
    3.3. Determinando el tipo del objeto:
    “typeof(item)” nos devuelve el tipo, en caso de objetos devuelve “object”, porque no son objetos nativos de JS.
    Cuando llamamos “new” nos crea una propiedad “item.constructor”. En esta propiedad esta todo el codigo del constructor.
    Si comparamos item.constructor == Marcianito. Y nos indica si constructor es igual que función.
    Si hacemos (marcianito1 instanceof Marcianito) nos indicara “true” porque nos indica que marcianito1 es una instancia del constructor Marcianito.
    Si hacemos (marcianito1 instanceof Object) tambien es “true” porque todos objetos heredan de Object.
  1. Prototipos
    4.1. Funciones compartidas en prototipos.
  • Es ineficiente crear cada vez funciones dentro asi:
    marcianito1.disparar = function () {
    this.disparos–;
    alert(this.name + ” ha disparado.”);
    }
  • Podemos asignar al metodo de la clase una funcion no anonima. Pero no es mejor forma de hacer lo.
    Ej.: marcianito1.disparar = marcianoDisparar;
  • En cambio podemos usar prototipo “Marcianito.prototype” (en este caso no se utiliza “this.”). Si le asignamos una función, sera disponible para todas las instancias de la clase. Ej.: Marcianito.prototype.disparar = function () {…}
  • Para no redefenir la misma funcion en cada llamada al constructor, comprobamos si ya existe: if(!Marcianito.prototype.disparar) { Marcianito.prototype.disparar = function() {} };
  • Con prototype podemos definir funciones a clases que ya existen. Por ejemplo queremos tener un check si es numerico en cadenas. Extendemos clase “String” y anyadimos con propotipo nueva funcionalidad. Ej.: String.prototype.isNumeric = function () {…}
    4.2. Búsqueda de miembros y cadena de prototipos:
  • No se definen metodos con prototipos en todas las clases instanciadas de un tipo en concreto. Se sigue una cadena de busqueda:
    A) Primero se busca en el objeto.
    B) Si no está se busca en su prototipo
    C) Si no está se busca en el prototipo del prototipo (de haberlo) y así sucesivamente.
    D) Si no se encuentra en el último prototipo entonces se produce un error porque no existe el miembro buscado.
  • Es muy importante que “this.” sigue apuntando al objeto original, aún que se llama desde “prototype”.
  • Prototipo es un objeto, asi que puede tener su propio prototipo.
  • “Shadow”: Podemos con prototipo ocultar en nivel inferior una funcionalidad superior. Ej: Marcianito.prototype.toString = function() { return this.name + ” – ” + this.color; };
  • Para crear variables estaticas compartidas entre todos objetos de la misma clase: Marcianito.prototype.Propiedad1 = 1; No es muy recomendable su uso.
  • Propiedad “proto” apunta a la clase prototipo. Normalmente no tenemos que tocarla.
  1. Controlando el valor de contexto con call y apply:
  • Valor de “this.” depende del contexto desde cual lo llamamos. Pero lo podemos modificar.
  • Usar metodos “call” y “apply” para pasar contexto como primer argumento y parametros (call) o matriz (apply). Ej.:
    function asignarNombreYEdad(nombre, edad) {
    this.Nombre = nombre;
    this.Edad = edad;
    }
    var miObjeto = new Object();
    asignarNombreYEdad.call(miObjeto, “Pepe”, “40”); // Llamada a la función usando call toma primer parametro como contexto (this)
    asignarNombreYEdad.apply(miObjeto, [“Pepe”, “40”]); // Lo mismo que “call”, pero se le pasan parametros en una matriz
    alert(miObjeto.Nombre + ” – ” + miObjeto.Edad);
  • Metodo “bind” igual que “call” pero no ejecuta directamente, sino crea una referencia a funcion para una llamada mas tarde con su contexto y parametros.
  1. Herencia
    6.1. Herencia: Encadenamiento de prototipos
  • No se soporta de manera nativa en JS.
  • Formas de herencia: simple encadenado de prototipos, robo de constructores, herencia por combinación, herencia prototípica (inventado por Douglas Crockford, conocido programador JavaScript), herencia parasitaria.
  • Si hacemos que el prototipo de nuestra clase sea una instancia de otra, entonces la nueva clase dispondrá de los mismos miembros que la original. Ej.:
    function ClaseBase()
    {
    this.Nombre = “”;
    ClaseBase.prototype.mostrarNombre = function () {
    alert(this.Nombre);
    }
    }
    function ClaseDerivada()
    {
    this.Apellidos = “”;
    }
    ClaseDerivada.prototype = new ClaseBase();
    ClaseDerivada.prototype.constructor = ClaseDerivada; // Importante asignar el constructor de esta manera

var cd = new ClaseDerivada();
cd.Nombre = “Perico”;
cd.Apellidos = “de los Palotes”;
cd.mostrarNombre();
alert(cd instanceof ClaseDerivada);
alert(cd instanceof ClaseBase);

No es muy recomendable esta tecnica antigua, porque es propensa de errores y no es muy comoda.

6.2. Robo de constructores y sustitución de prototipos:

  • Bibliotecas utiles: jQuery, Dojo, Prototype, ExtJS.
  • Herencia aconsejable en JS: robo de constructores con sustitución de prototipos. Consiste en llamar al constructor de la clase base desde el constructor de la derivada, pasándole como contexto el objeto que se está creando. Ej.:
    function ClaseBase(nom)
    {
    this.Nombre = nom;
    ClaseBase.prototype.mostrarNombre = function () {
    alert(this.Nombre);
    };
    }
    function ClaseDerivada(nom, apell)
    {
    ClaseBase.call(this, nom);
    this.Apellidos = apell;
    }
    ClaseDerivada.prototype = new ClaseBase();
    ClaseDerivada.prototype.constructor = ClaseDerivada;

6.4. Detalles internos sobre creación de objetos
Ej.:
function NEW(fConstructora){
var nObjeto = {
proto‘: fConstructora.prototype
};
return function(){
fConstructora.apply(nObjeto, arguments);
return nObjeto;
};
};

  • Se asigna prototipo.
  • Se devuelve funcion como objeto.
    Constructor normal:
    function Animal(sNombre, nPatas) {
    this.nombre = sNombre;
    this.patas = nPatas;
    }
    var bicho = new Animal(“Perro”, 4);
    console.log(bicho);
    Con nuestro constructor:
    var bicho2 = NEW(Animal)(“Canario”, 2);
    console.log(bicho2);
    Herencia habitual:
    function Gato(sNombre) {
    Animal.call(this, sNombre, 4);
    }
    Gato.prototype = new Animal();
    Gato.prototype.constructor = Gato;
    var gato1 = new Gato(“Patucas”);
    console.log(gato1);
    Herencia con nuestro constructor:
    var gato2 = NEW(Gato)(“Calcetines”);
    console.log(gato2); //Idéntico!

6.5. Herencia: Detalles importantes a recalcar

  • Clase puede derivarse de otra clase.
  • Los prototipos no se utilizan para instanciar objetos, sino en si son objetos.
  • Si objeto no tiene propiedad, JS busca en sus prototipos padres. Uso de cadena de prototipos.
  • Clase B que herada de clase A no tiene su constructor por defecto. Hay que asignar lo (robar), pero no siempre es obligatorio.
  • Propiedades prototype y proto no son iguales. Uno es para definicr cosas, otro es para ver el objeto. Normalmente objetos no disponen de propiedad prototype cuando depuramos, prototype apunta a objeto patron, en cambio proto al objeto concreto. “prototype” es propiedad solo de tipos nativos de JS.
  • Clase en JS en realidad en una funcion constructora.
  • Cuando una función se usa con la palabra clave new delante, JS crea un objeto vacío, asigna como prototipo (en proto) lo que haya en la propiedad prototype de la función y la llama estableciendo this a este nuevo objeto.
  • ClaseDerivada.prototype.constructor = ClaseDerivada es para asociar y saber en futuro el tipo de objeto/funcion constructora. Sino tambien funciona, pero no sabremos el tipo. O tambien para instanciar otra clase sin igual sin saber cual es la clase exacta con .constructor. Esta propiedad no existe si creamos objeto con JSON, no es obligatoria para objetos, ya que JS no la utiliza.

6.6. Herencia: Uso práctico de prototype

  • Propiedad prototype contiene funcion que se utilizara para crear objetos que se asignaran a la propiedad “proto“.
  • El prototipo es el mismo para todos los objetos de la misma clase.
  • Podemos aprovechar prototipo para crear metodos y propiedades “estaticas”.
  • Formas de crear “prototype”:
    A. Gato.prototype = new Animal(); // Podemos tener problemas con propiedades que no inicializamos
    B. Gato.prototype = new Animal(“Patron”, 0); // evitamos problemas de que haya propiedades sin inicializar
    C. Gato.prototype = object.Create(Animal.proptotype); // variante moderno, utilizando Object.create, crea objeto sin inicializar con prototipo asignado, no se llama al constructor de la clase base. Mas optimizado por no tener objetos no inicializados. Es el mas recomendado si IE>8
  • Asignamos construcor “Gato.prototype.constructor = Gato;” para coherencia y para crear hijos:
    Animal.prototype.procrear = function(){
    // Creacion normal
    return new Animal(this.nombre + ” – Hijo”, this.patas);
    // Creacion usando constructor original y le asigna el tipo de objeto identico al del padre
    return new this.proto.constructor(this.nombre + ” – Hijo”, this.patas);
    };
    gatito1 = gato1.procrear();
    console.log(gatito1);
  • Nuevas versiones de firefox muestran “proto” como “”, pero realmente es “proto“.

7.0. Encapsulación en JavaScript

  • La encapsulación o encapsulamiento es una característica de los lenguajes orientados a objetos que consiste en que las clases pueden definir miembros privados, ocultos al exterior, y que solamente pueden ser cambiados a través de los métodos establecidos a tal efecto.
  • En JS es dificil. Ya que propiedades son publicas.
  • clausuras o closures. Es una función que es evaluada en un determinado ámbito pero que tiene acceso también a variables que están ubicadas en un ámbito diferente. Son como un puente entre dos mundos.
    Ej.:
    function pruebaClosure() {
    var loc = “¡Hola closure!”;
    this.muestraMensaje = function () {
    alert(loc);
    };
    }
    var obj = new pruebaClosure();
    obj.muestraMensaje(); //Muestra el mensaje de saludo
  • La verdadera regla de acceso a variables es que una línea de nuestro código sólo puede acceder a variables que tengan un ámbito mayor o igual que el suyo propio. En este caso “loc” pertenece a un ambito mayor.
  • una jerarquía donde los niveles inferiores siempre tienen acceso a los superiores.
  • lo que representan son funciones que tienen acceso a una serie de variables liberadas ya en su propio ámbito original, pero que quedan confinadas (clausuradas) en el ámbito de la función que las utilizará más tarde, fuera de ámbito.

Ejecución diferida de funciones gracias a las clausuras
function concatenar(s1) {
return function(s2) {
return s1 + ‘ ‘ + s2;
};
}
var diHola = concatenar(“Hola”);
alert( diHola(“visitante”) );

  • hemos definido una función que permite asignar valores para ejecución retardada.
  • lo que tenemos en la variable diHola es una función pre-parametrizada y lista para ser utilizada.

Ejemplo de uso en setInterval y setTimeOut cuando queremos pasar parametros a la funcion, en lugar de llamar a una funcion desde otra funcion (solución fea pero comun).
function moverElemento(elto, x, y)
{
return function(){
elto.style = “position:absolute; left:”+ x + “;top:” + “y;”;
};
}
var mover = moverElemento(getElementById(“miDiv”), 0, 0);
setTimeout(mover, 500);

  • Para mantener ambito al principio de funciones escribimos como primera linea, es importante para si hay subfunciones:
    var that = this; // O tambien se utiliza “var self = this;”

7.2.1. Prototipos accediendo a variables de ámbito: un error grave

  • Cuidado que objetos no tengas atributos compartidos:
    function Marcianito(elNombre, elColor, posX, posY, disparosIniciales) {
    var that = this;
    var _name = elNombre;
    Marcianito.prototype.getName = function(){
    return _name.toUpperCase() + ” (” + that.color + “)”;
    };
    }
  • Aqui el problema esta en que se redefine funcion de prototipo “getName”. Esta funcion del prototipo queda atada a propiedades del ultimo ambito que la llama. De esta manera podemos crear “variables privadas estáticas”.
  • Para metodos de clausura no hay que utilizar prototype, sino crear metodos con this. Ej: this.getName = function(){…_name…that.color…;
  • Las funciones del prototipo no pueden acceder a variables de instancia (como _name o that en el ejemplo).
  • Funcion completa de ejemplo:
    function Marcianito(elNombre, elColor, posX, posY, disparosIniciales) {
    var that = this;
    var _name = elNombre;
    this.getName = function(){
    return _name.toUpperCase() + ” (” + that.color + “)”;
    };
    this.setName = function (nombre) {
    _name = nombre;
    };
    this.color = elColor;
    if (posX < 0) posX = 0; if (posX > 100) posX = 100;
    this.x = posX;
    if (posY < 0) posY = 0; if (posY > 100) posY = 100;
    this.y = posY;
    if (disparosIniciales < 0) disparosIniciales = 0; if (disparosIniciales > 100) disparosIniciales = 100;
    this.disparos = disparosIniciales;
    if (!Marcianito.prototype.disparar) {
    Marcianito.prototype.disparar = function () {
    this.disparos–;
    //codigo para pintar el disparo
    alert(this.getName() + ” ha disparado.”);
    };
    }
    Marcianito.prototype.toString = function () {
    return this.getName() + ” – ” + this.color;
    };
    }

7.3. Verdaderas propiedades encapsuladas

  • En POO por regla general se entiende por propiedad a un miembro que utiliza el mismo nombre tanto para leer como para escribir.
  • defineGetter y defineSetter de Object para definir con que funcion escribir y leer propiedades. Integradas en Mozilla Firefox, Chrome, Safari y Opera, pero no en Internet Explorer. Mozilla ha decidido hacerlas obsoletas, no forman parte de estandar.
  • ECMAScript 5 introduce “método defineProperty”.
  • defineProperty(objeto, nombre, descriptor):
    A. El objeto sobre el que se va a definir la propiedad
    B. El nombre de la propiedad
    C. Un objeto descriptor que definirá mediante sus propiedades las características concretas que regirán el acceso a la propiedad.
    C1. value: el valor inicial.
    C2. get: leer valor.
    C3. set: almacenar valor.
    C4. writable: posible cambiar valor.
    C5. configurable: si podemos o no cambiar el tipo de descriptor.
    C6. enumerable: indica se es enum.
  • “delete” permite eliminar miembros de objetos.
  • Ejemplo de uso de defineProperty
    var objeto = new Object();
    Object.defineProperty(objeto, “nombre”,
    {
    value: “Mi nombre”,
    writable: true,
    enumerable: true,
    configurable: true
    }
    );
  • Otra forma de definir con JSON
    if (Object.defineProperty) {
    Object.defineProperty(this, “name”,
    {
    get: getName,
    set: setName
    });
    }
  • Si usamos value no podemos utilizar las funciones get y set, y viceversa.
  • variante denominada defineProperties (en plural) para definicion multiple:
    Object.defineProperties(objeto,
    {
    “Apellido1”: { value: “López”, writable:true, enumerable:true},
    “Apellido2”: { value: “Fernández”, writable: true, enumerable: true }
    }
    );
  • Podemos bloquear la modificación de un objeto con Object (preventExtensions, seal y freeze).
  • “preventExtensions” no deja añadir propiedades o metodos.
  • “sel” impide añadir, quitar, redefinir propiedades/metodos.
  • “freeze” sella objeto y conjela propiedades (si en descriptor usan “value”).
  • Para verificar estado de estos metodos isExtensible, isSealed y isFrozen.
  1. Reflexión: Inspeccionando los objetos
  • Técnicas de reflexión (miembros que tiene un objeto)
  • Nombres (for-in): for (var prop in marcianito1)
  • Acceso a valor: marcianito1[“disparos”] = 50;
  • Llamada a funcion: marcianito1[“disparar”]();
  • For-in no devuelve en mismo orden en diferentes navegadores. Propiedades sin definir pueden no aparecer. Ej.:
    function ResumeObjeto(obj) {
    var prop;
    var informe = “”;
    for (prop in obj) {
    informe += prop + “: ” + obj[prop] + “\n”;
    }
    return informe;
    }