Introducción a la Programación Orientada a Objetos
Aprende a programar y abre las puertas a un mundo de nuevas oportunidades laborales. Empieza desde lo básico y construye una carrera con alta demanda. ¡Regístrate ahora!
Quiero mejorar mi futuro
Cualquier programador de JavaScript sabe que casi todo en este lenguaje de programación es un objeto: desde matrices hasta funciones, desde expresiones regulares hasta fechas. Podemos decir que lo que no es un tipo de dato primitivo es un objeto. Pero, incluso los tipos de datos primitivos como números o cadenas tienen contenedores de objetos, es decir, son accesibles a través de objetos. Por lo tnato, podemos argumentar que los objetos son vitales en JavaScript y que debemos aprender a usarlos lo mejor que podamos para crear mejores aplicaciones. Una forma, por ejemplo, de usar mejor los objetos es aplicar el paradigma de Programación orientada a Objetos (POO).
Sin embargo, antes de sumergirnos en los principios y patrones de la POO, comencemos por un repaso rápido sobre los fundamentos de los objetos. En particular:
- Cómo crear y gestionar objetos literales
- Cómo definir constructores de objetos
- Qué es un prototipo y cómo utilizarlo
- La nueva construcción de clases de ECMAScript 2015 y su relación con objetos, constructores y prototipos.
Fundamentos de objetos en JavaScript
Cómo crear y gestionar objetos literales
Un objeto es un contenedor de valores combinados para formar una única estrucutra de datos que tiene una identidad particular. Normalmente , de hecho, un objeto se utiliza para representar una entidad específica, como una persona, un pedido, una factura, una reserva, etc., a través de una agregación de datos y funcionalidades.
Los datos se denominan propiedades y se representan mediante pares de nombres y valores. Las funcionalidades se denominan habitualmente métodos y se representan mediante funciones, aunque no sean más que los mismos pares de nombres y valores que para las propiedades, donde los valores resultan ser funciones.
La forma sencilla de crear un objeto en JavaScript es la representación literal, como se muestra en el siguiente ejemplo:
var objetoVacio = {};var persona = { "nombre": "Elía", "apellido": "Cuevas"}A través de la notación literal, representamos un objeto encerrando sus propiedades y métodos entre llaves. En la primera sentencia, creamos un objeto vacío, un objeto sin propiedades ni métodos; en definitiva, no muy útil pero importante para entender que un objeto es básicamente una lista de pares de valores y, como toda lista, puede estar vacío.
En la segunda declaración, para definir el objeto persona, enumeramos dos pares de strings separados por comas. Cada par está conformado por dos strings separados por el símbolo de dos puntos :. El primer string es el nombre de la propiedad mientras que el segundo representa su valor.
Propiedades
Para asignar nombres a las propiedades de un objeto, no tenemos las mismas restricciones como para declarar los nombres de las variables en JavaScript. Podemos utilizar cualquier string, aún cuando existe alguna restricción al momento de acceder a la propiedad con un nombre en particular, así como lo veremos más adelante.
Las comillas dobles o simples alrededor del nombre de la propiedad son generalmente opcionales, pero se necesitan cuando el nombre no respeta las reglas de nomenclatura de variables. Es decir, podemos crear nuestro objeto person de la siguiente manera:
var persona = { nombre: "Elías", apellido: "Cuevas"};Pero si necesitamos crear un objeto de la siguiente manera, debemos utilizar comillas dobles o simples en el nombre de nuestras propiedades:
var persona = { "primer-nombre": "Juan", "segundo-nombre": "Carlos"};Podemos asignarle cualquier valor a las propiedades de nuestros objetos, incluyendo otro objeto. Es decir, podemos anidar objetos:
var persona = { nombre: "Elías", apellido: "Cuevas", direccion: { calle: "De Elías", ciudad: "Donde Vive Elías" }}Como podemos observar, un objeto con es asignado a la propiedad direccion.
Para acceder a los valores almacenados en la propiedad de un objeto, tenemos dos opciones. La primera opción se le conoce como dot-notation (Notación de punto), en la que accedemos a la propiedad que deseamos agregando un punto después del objeto que vamos a utilizar:
var nombre = persona.nombre;Esta es la manera más común porque es muy parecida a la mayoría de los lenguajes de programación modernos.
La segunda opción es escribiendo corchetes y agregando en nombre de la propiedad utilizando comillas dobles o simples dentro de los corchetes:
var nombre = persona["nombre"];Esta segunda opción es obligatoria cuando el nombre de la propiedad no sigue las reglas de nomenclatura de variables de JavaScript. Por ejemplo, no podemos utilizar dot-notation para acceder a una propiedad llamada first-name.
Si intentamos acceder a una propiedad que no existe en un objeto, no se genera ningún error, pero obtenemos undefined como resultado.
var edad = persona.edad;Si intentamos asignarle un valor a una propiedad que no ha sido definida, se crea la propiedad con el valor que le asignamos:
persona.edad = 100;Este ejemplo nos muestra la naturaleza dinámica de los objetos de JavaScript. La estructura de los objetos es muy flexible y puede ser modificada dinámicamente durante la ejecución de un script. Esta funcionalidad nos da una forma alternativa de crear un objeto de forma incremental. En la práctica, en lugar de definir completamente nuestros objetos, podemos empezar por una representación básica y gradualmente completarlo con propiedades que deseemos. Siguiendo este enfoque, podemos definir nuestro objeto persona de la siguiente manera:
var persona = {};persona.nombre = "Esteban";persona.apellido = "González";perona.direccion = { calle: "De Elías", ciudad: "Donde Vive Elías"};persona.edad = 100;Además de ser capaces de crear dinámicamente las propiedades de nuestros objetos, también podemos destruirlas en tiempo de ejecución utilizando la declaración delete. En el siguiente ejemplo se muestra cómo eliminar la propiedad direccion de nuestro objeto persona.
delete persona.direccion;Métodos
Mientras que las propiedades de los objetos representan datos, los métodos representan acciones que un objeto puede realizar. Si revisamos su sintáxis, la definición de un método de un objeto es algo similar a la definición de una propiedad. Por ejemplo:
function mostrarNombreCompleto() { return "Jeremías Quiróz";}
persona.nombreCompleto = mostrarNombreCompleto;También podemos asignar un método a un objeto dentro de su representación literal como se muestra a continuación:
var persona = { nombre: "Jeremías", apellido: "Quiróz", mostrarNombreCompleto: function() { return "Jeremías Quiróz"; }};ECMAScript2015 nos permite definir métodos en notación literal de una forma más directa:
var persona = { nombre: "Jeremías", apellido: "Quiróz", mostrarNombreCompleto() { return "Jeremías Quiróz"; }};De hecho, la distinción entre propiedades y métodos es de alguna manera artificial en JavaScript. El hecho es, dado que las funciones también son objetos, un método no es nada más que una propiedad que tiene una función asignada.
Incidentalmente, dado que el valor asignado a una propiedad puede cambiar en tiempo de ejecución, es posible que una propiedad, a la que inicialmente se le asignó una función, después se le asigne un valor diferente, como un string o un number. A diferencia de otros lenguajes de programación, un método no es una condición estable para un miembro de un objeto, no es una característica de una propiedad específica. Podemos simplemente afirmar que una propiedad es un método cuando tiene una función asignada, pero su condición es dinámica.
En el ejemplo anterior, definimos una función que simplemente regresaba un string y asignamos ese nombre de la funci’ón a una nueva propiedad del objeto persona. Es importante notar que no estamos asignando el resultado de la función, sino la función.
La propiedad nombreComleto, dado que se le fue asignada una función, es de hecho un método. Para ejecutarlo, debemos acceder a él agregando paréntesis como una función cualquiera:
var nombreYApellido = persona.nombreCompleto();Por supuesto, podemos asignar funciones anónimas a una propiedad directamente:
persona.nombreCompleto = function () { return "Jeremías Quiróz";}El método que acabamos de definir en el ejemplo no es muy útil. Siempre mostrará el mismo string aún si asignamos un valor diferente a las propiedades nombre y apellido del objeto persona. Sería útil tener un modo de mostrar el valor actual de las propiedades.
JavaScript nos permite conseguirlo a través de la palabra reservada this. this representa al objeto cuando el método es invocado. Entonces, podemos rescribir nuestro método de la siguiente manera:
persona.nombreCompleto = function() { return this.nombre + " " + this.apellido;};Esto nos garantisa mostrar el valor actual de nuestrar propiedades del objeto que estemos utilizando.
Object constructors
La creación de un objeto, como vimos en los ejemplos, es muy fácil si utilizamos la notación literal. No necesitamos definir una clase, sólo creamos directamente el objeto cuando lo necesitamos, y afortunadamente podemos cambair su estructura durante la ejecución de nuestro script.
Ahora, supangamos que necesitamos múltiples objetos que tengan el mismo tipo, por ejemplo más objetos personas, que compartan la misma estructura.
Utilizando la notación literal, tendremos que repetir la definición por cada objeto que queramos crear, lo cuál es esencial para identificar a una persona en particular pero innecesariamente repetitivo para miembros constantes como los métodos. En otras palabras, utilizando la notación literal tendremos un resultado no reutilizable. Por ejemplo, si queremos crear dos objetos persona, necesitamos escribir el siguiente código:
var jeremíasQuiróz = { nombre: "Jeremías", apellido: "Quiróz", direccion: { calle: "De Jeremáis", ciudad: "Donde Vive Jeremías" }, mostrarNombreCompleto = function() { return this.nombre + " " + this.apellido }};var marioOzalba = { nombre: "Mario", apellido: "Ozalba", direccion: { calle: "De Mario", ciudad: "Donde Vive Mario" }, mostrarNombreCompleto = function() { return this.nombre + " " + this.apellido }};Por lo tanto, para poder evitar definir objetos con la misma estructura Desde0, podemos utilizar un constructor - una función de JavaScript que se invoka utilizando el operador new. Veamos como podemos crear un constructor para el objeto persona:
function Persona() { this.nombre = ""; this.apellido = ""; this.direccion = ""; this.correoElectronico = ""; this.mostrarNombreCompleto = function() {...};}Esta función define las propiedades de nuestro objeto asignandolas a la palabra reservada this y asignando valores por defecto. Aún cuando este constructor pueda parecer innecesario dado que asigna sólo strings vacíos a las propiedades, también define una estructura en común para cualquier objeto creado por este constructor. Así que, para crear un objeto de tipo persona, debemos llamar la función utilizando el operador new antes:
var jeremíasQuiróz = new Persona();jeremíasQuiróz.nombre = "Jeremías";jeremíasQuiróz.apellido = "Quiróz";
var marioOzalba = new Persona();marioOzalba.nombre = "Mario";marioOzalba.apellido = "Ozalba";De esta manera, cuando queramos crear múltiples objetos con la misma estructura, nos limitaremos a nosotros mismos a asignar sólo los valores específicos que distinguen uno de nuestros objetos de los demás.
Como podemos observar, el nombre dato a nuestro constructor empieza con mayúscula. Esto no es necesario para JavaScript, pero es una convención utilizada para ayudar a distinguie las funciones regulares de los constructores.
En la definición de un constructor, podemos esperar la presencia de parametros que puedan ser utilizados para inicializar nuestro objeto. Por ejemplo:
function Persona(nombre, apellido) { this.nombre = nombre; this.apellido = apellido; this.direccion = ""; this.correoElectronico = ""; this.mostrarNombreCompleto = function () {...};}Esto nos permite crear e inicializar un objeto especificando valores directamente en la llamada del constructor:
var jeremíasQuiróz = new Persona("Jeremías", "Quiróz");var marioOzalba = new Persona("Mario", "Ozalba");Es muy importante utilizar el operador new cuando creamos un objeto a través de un constructor. En realidad, si olvidamos utilizarlo, lo que obtendremos no será la creación de un objeto, pero la ejecución de una función, con resultados impredecibles. Por ejemplo, supongamos que intentamos crear una objeto persona omitiendo el operador new:
var jeremíasQuiróz = Persona();El valor de la variable jeremíasQuiróz será undefined, dado que la función Person() no regresa ningún valor. Además, todas las propiedades y métodos definidos en el cuerpo de la función serán asignados a un objeto representado por la palabra reservada this en la ejecución del contexto de la función, lo que podría ser por ejemplo el objeto global window de nuestro navegador. Esta asignación puede redefinir valores de variables con el mismo nombre causando efectos adversos que dificulten la predictibilidad al momento de depurar nuestro código.
Podemos reducir este riesgo adverso utilizando strict mode:
function Persona() { "use strict"; ...}En strict mode, el valor del objeto representado por la palabra reservada this es undefined durante la ejecución de la función. Esto genera un error en tiempo de ejecución cuando intentamos acceder a la propiedad de un objeto cuando no existe, así evitamos invocaciones no deseadas al constructor.
Desafortunadamente, esta forma no es suficiente cuando el constructor está definido dentro de un namespace:
var humanidad = { ... Persona: function(nombre, apellido) { 'use strict'; this.nombre = nombre; this.apellido = apellido; }};
var jeremíasQuiróz = humanidad.Persona("Jeremías", "Quiróz");En este caso, la palabra reservada this representa al objeto humanidad, así que no tendremos un error en tiempo de ejecución que nos advierta sobre el uso incorrecto del construcotr, sino que las propiedades nombre y apellido serán asignada al objeto humanidad.
El constructor Object()
Hemos visto como el uso de un constructor nos ofrece un alto nivel de abstracción en la creación de objetos. En esta sección, exploraremos un constructor particular proveniente de JavaScript, el constructor Object().
Este constructor nos permite crear un objeto genérico:
var persona = new Object();persona.name = "Francisco";persona.apellido = "Lainez";En este ejemplo, utilizamos el operador newpara crear una instancia de un objeto vacío y luego creamos propiedades asignandole valores, así como en la notación literal.
De hecho, al crear un objeto utilizando la notación literal o creandolo a través del constructor Object() es lo mismo. Cada objeto creado utilizando la notación literal utiliza Object() de forma implicita. Podemos comprobarlo accediendo la propiedad constructorde cada objeto:
var persona = {};console.log(persona.constructor === Object); // trueEl constructor Object() también es capaz de generar intancias de objetos desde cualquier expresión JavaScript:
var numero = new Object(100);var otroNumero = new Object(2*2);var texto = new Object("Hola mundo");var persona = new Object({ nombre: "Francisco", apellido: "Lainez" });A diferencia de la última sentencia, la es equivalente a la creación de un objeto a través de su representación literal, las primeras tres sentencias crean un objeto desde un tipo de dato primitivo, como un número o un texto. El resultado no es solo un valor numérico o de texto, sino un objeto especializado en manejar valores numericos y de texto.
Para entender más a profundidad el constructor Object(), podemos revisar su definición en MDN.
Prototypes de Object
La flexibilidad de los objetos en JavaScript es expresada principalmente a través de la capacidad de cambiar su estructura aún después de su creación. Aún utilizando un constructor para crear un objeto, seguimos tenidno esta posibilidad. Por ejemplo, podemos escribir el siguiente código:
var franciscoLainez = new Persona("Francisco", "Lainez");var marioOzalba = new Persona("Mario", "Ozalba");
franciscoLainez.saludos = function() { console.log("Hola " + this.name + " " + this.apellido);};Este código creará un nuevo método saludos() para el objeto franciscoLainez sin afectar la estructura de marioOzalba.
Básicamente, al crear objetos, podemos comenzar desde una estructura común definida por su constructor y luego personalizarla a nuestras necesidades.
Pero, ¿cómo podemos cambiar la estructura de todos los objetos creados utilizando un constructor? Por ejemplo, supongamos que después de crear un objeto utilizando el constructor Persona(), queremos que todas las intancias de Persona tengan un método llamado saludos(). Podemos lograrlo a través de una de las funciones más interesantes de la programación orientada a objetos en JavaScript, el prototype.
En nuestro caso, lo haremos de la siguiente manera:
Persona.prototype.saludos = function() { console.log("Hola " + this.name + " " + this.apellido);};Esta sentencia indica que todos los objetos creados utilizando el constructor Persona() tendrán instantáneamente disponible también el método saludos().
Para ser precisos, el nuevo método no se añade directamente a cada objeto, sino que es accesible como si fuera su método. Esto es posible gracias al mecanismo de prototipos de JavaScript. Este mecanismo es la base de la herencia en Programación Orientada a Objetos en JavaScript.
En JavaScript, el prototype de un objeto es un tipo de referencia de otro objeto. Los objetos creados a través de la notación literal referencian implícitamente al constructor Object() como su prototipo.
Cuando creamos un objeto utilizando un constructor, su objeto prototipo es el prototipo del constructor.
Si intentamos acceder a una propiedad o método de un objeto que el objeto por si mismo no tiene, JavaScript lo buscará en las propiedades y métodos de su prototype. Por ejemplo, en nuestro ejemplo anterior, si intentamos acceder al método saludos() de nuestro objeto marioOzalba, JavaScript no lo encuentra entre los métodos, pero lo encontrará entre los métodos de su prototype.
El prototype de un objeto puede a su vez tener otro prototype. En este caso, la búsqueda de una propiedad o método va a seguir la cadena de prototipos hasta que se encuentre o hasta que se llegue al final de la cadena.
Los objetos nativos de JavaScript tienen una referencia a su prototype también. En la mayoría de los casos, su manejo es muy similar al manejo de prototipos de objetos creados a través de constructores. Esto nos permite extender funcionalidades no propias de los objetos nativos de una forma muy elegante.
Por ejemplo, si queremos crear un método para agregar un espacio disponible para todos los objetos de tipo String, podemos hacerlo de la siguiente manera:
String.prototype.espacioALaIzquierda = function(ancho, caracter) { var resultado = this; caracter = caracter || ' ';
if (this.length < ancho) { resultado = new Array(ancho - this.length + 1).join(caracter) + this; } return resultado;}Con esta definición podemos utilizar espacioALaIzquierda() en cualquier string de JavaScript:
var texto = "Hola";var textoConEspacio = texto.espacioALaIzquierda(10, "0");console.log(textoConEspacio); // "000000Hola"Utilizando clases
Hasta ahora, hemos creado objetos utilizando dos mecanismos: La notación literal y los constructores. Ambos mecanismos nos permiten crear objetos de una forma muy sencilla, sin muchas formalidades.
Sin embargo, la mayoría de los desarrolladores están acostumbrados a crear objetos desde el constructor de una clase. El hecho es que muchos lenguajes de Programación Orientada a Objetos permiten que los desarrolladores definan clases y creen objetos utilizando esas clases.
Las especificaciones de ECMAScript 2015 (También conocidas como ECMAScript 6 o ES6) introdujeron el constructor classen JavaScript también. Sin embargo, este constructor no tiene nada que ver con las clases tradicionales del paradigma de Programación Orientada a Objetos.
Mientras que en otros lenguajes, como Java o C#, una clase es una representación abstracta de la estructura de un objeto, en JavaScript, el contructor class es solo una simplificación sintáctica para crear objetos de la forma que ya vimos anteriormente. El constructor class de JavaScript provee una sintaxis más simple y clara de manejar constructores, prototipos y herencia.
El nuevo constructor class crea un orden entre las diferentes formas de crear objetos y nos insita a aplicar mejores prácticas el manejo de prototipos.
Veamos como definir una clase utilizando el constructor class:
class Persona { constructor(nombre, apellido) { this.nombre = nombre; this.apellido = apellido; }}Esta clase define un constructor para objetos de tipo Persona. Esto es totalmente equivalente con el viejo estilo de JavaScript:
function Persona(nombre, apellido) { "use strict"; this.nombre = nombre; this.apellido = apellido;}Si utilizamos la sentencia typeof podemos comprobar que Persona sigue siendo una función:
console.log(typeof Persona); // "function"Podemos crear un objeto utilizando la clase Persona de la misma manera que lo hacemos con los constructores tradicionales:
var franciscoLainez = new Persona("Francisco", "Lainez");Sin embargo, a diferencia de un constructor, no podemos invocar una clase como una función:
var persona = Persona("Francisco", "Lainez"); // TypeError: Class constructor Persona cannot be invoked without 'new'Esto asegura que no tengamos el riesgo de los efectos adversos que afectan a los constructores tradicionales.
Podemos asignar la definición de una clase a una variable y después utilizar la variable como un constructor:
const Persona = class { constructor(nombre, apellido) { this.nombre = nombre; this.apellido = apellido; }}Desde un punto de vista sintáctico, una clase es una colección de métodos dentro de un bloque {} identificado por su nombre.
Uno de estos métodos es el constructor(), cuya tarea es definir e inicializar las propiedades del objeto:
class miClase { constructor(valor1, valor2) { this.valor1 = valor1; this.valor2 = valor2; this.valor3 = ""; }
metodo1() { ... }
metodo2() { ... }}El constructor de una clase es un método con el nombre reservado constructor. No puedes tener más de un método constructor por clase.
El método constructor retorna, por defecto, la nueva instancia si no tiene un return explícito, como un constructor tradicional en JavaScript. Sin embargo, es posible sobreescribir el comportamiento por defecto insertando un return el el cuerpo del constructor:
class MiClase { constructor(valor) { this.valor = valor; return { propiedad1: valor, propiedad2: "" }; }}
var instancia = new MiClase("Hola");Todos los métodos definidos en una clase se añaden a su prototype. La propiedad prototype de una clase es el prototipo que los objetos creados con dicha clase heredarán.
La elección de agregar métodos al prototype de una clase es el resultado de aplicar una mejor práctica común. Dado que usualmente métodos no son dinámicamente actualizados, al agregarlos al prototype, esto nos permite optimizar el manejo de memoria. De hecho, si los métodos no son agregados al prototype, entonces deben de ser replicados en cada nuevo objeto creado con una necesidad de memoria proporcional. Los métodos agregados al prototype asegura que solo tengamos una sola copia de los métodos en memoria.
A diferencia de las funcioens, las clases no son hoisted. Esto significa que mientras que una función puede ser utilizada antes de su definición, una clase no:
var persona = new Persona(); // ReferenceError: Persona is not defined
class Persona { constructor(nombre, apellido) { this.nombre = nombre; this.apellido = apellido; }}Estas funcionalidades de las clases descritas son las básicas. Si quieres aprender más sobre clases en JavaScript, puedes revisar la documentación oficial de MDN.
Resumen
En esta clase hemos visto algunos conceptos relacionados con la administración de objetos en JavaScript.
Exploramos dos formas de crear objetos: La notación literal y los constructores. El primero es muy simple pero no tan práctico cuando necesitamos generalización, mientras que el segundo es un poco más completo pero efectivo.
También vimos el nuevo constructor class y analizamos como simplifica la definición de constructores de objetos y el uso de prototipos.
Ahora estás listo para explorar más a fondo los objetos en JavaScript. En las siguientes clases utilizaremos lo aprendido, así que siempre puedes volver a este tema para repasar los conceptos.
Aprende a programar y abre las puertas a un mundo de nuevas oportunidades laborales. Empieza desde lo básico y construye una carrera con alta demanda. ¡Regístrate ahora!
Quiero mejorar mi futuro