Programación Orientada a Objetos – Guía práctica – Parte #1

Introducción a los objetos

Un objeto en JavaScript (en adelante, JS) es parecido a un objeto de la vida real. Por ejemplo, una cámara de fotos. Tiene propiedades, como su color, su peso, y puede hacer cosas, como sacar fotos. Los objetos de JS también tienen propiedades y métodos para permitirles hacer cosas.

Vamos a trabajar directamente en la consola del navegador para explicar algunos conceptos. Ahí escribimos un array con algunos strings. 

>  const names = ['mike', 'dustin'];
<· undefined

Si escribimos esa variable en la consola, ésta nos devuelve las propiedades y métodos asociados a esa variable.

names
(2) ["mike", "dustin"]
    0: "mike"
    1: "dustin"
    length: 2
    __proto__: Array(0)
      concat: ƒ concat()
      constructor: ƒ Array()
      copyWithin: ƒ copyWithin()
      entries: ƒ entries()
      every: ƒ every()
      .
      .
      .

Todos los métodos asociados a un objeto en concreto se detallan dentro de "__proto__". En este caso, la consola nos devuelve todos los métodos aplicables a un array, porque ese es el​ data type de la variable names.

Esta no es la única manera de crear un array. Podríamos usar el array constructor en su lugar, haciendo uso de la keyword "new". Hagamos otro ejemplo con una variable nueva (ages). Si escribimos la variable, la consola nos devolverá sus propiedades y métodos, igual que en el ejemplo de arriba.

> const ages = new Array(16,86,56);
undefined
> ages
(3) [16, 86, 56]
    0: 16
    1: 86
    2: 56
    length: 3
    __proto__: Array(0)
Cuando utilizamos la keyword "new" para crear algo, el resultado siempre será algún tipo de objeto. 🤓

La versión donde usamos los corchetes es la más habitual para crear arrays. ☝

También podemos construir objetos con el primer sistema:

const userOne = {}

Y eso nos dará un objeto con todos sus métodos asociados. O podemos crear un object literal usando la keyword "new", lo cual nos daría el mismo resultado en la consola.

> const userTwo = new Object();
<· undefined
> userTwo
<· {}
    > __proto__: Object

🧐 Estas son las dos maneras en las que podemos crear algo: usando literal syntax o la keyword "new+ un constructor.

"Todo en JavaScript es un objeto" ¿O no?

Cuando empecé a aprender programación, escuchaba esta frase por todas partes: "Everything in JavaScript is an object" 🤷‍♀️. No entendía muy bien lo que significaba. Hasta ahora. Para empezar, si examinamos los diferentes tipos de datos en JS, tenemos los primitive values y los references typesSabiendo esto, debería estar claro que los primitive values NO son objetos, porque no entran dentro de esa categoría.

Entonces, ¿por qué se dice que "todo en JS es un objeto"?  🤨

Si escribimos en la consola una variable que guarda un string, veremos que no nos devuelve ninguna propiedad o método.

>  const name = 'Lucy';
<· undefined
>  name
<· "Lucy"

A pesar de ello, podemos usar las propiedades y métodos asociados a los strings, aunque no podamos verlos. ¿Pero por qué?  🤔

>  name.length
<· 4
<  name.toUpperCase()
<· "LUCY"

Lo que ocurre cuando intentamos acceder a una propiedad o método de un primitive value es que JS lo "envuelve" temporalmente en un objeto. Ese es el objeto que alberga temporalmente las propiedades y métodos de un string. Así es como JS los pone a nuestra disposición. 😮

Cuando hemos terminado de aplicarle propiedades o métodos a nuestro string, JS le quita el "envoltorio" de objeto en el que había envuelto el string y nos lo devuelve de nuevo como un primitive value. Esto es un mecanismo interno de JS invisible para nosotros. 👻

Vamos a investigar un poco más e intentar ver ese "envoltorio" en acción. Esta es la manera en la podríamos crear un envoltorio para un string:

  const nameTwo = new String('Rino');
  undefined
  nameTwo
 > String {"Rino"}

Y ahí podemos ver todas las propiedades y métodos a los que tenemos acceso, porque hemos creado un objeto, que en realidad es un string, pero envuelto en un objeto temporalmente. Prueba a hacer lo mismo con el resto de data types, creando tus propios "objetos envoltorios" de booleans, números, etc. 🎁

Sólo hay una excepción a todo esto: los primitive types "nully "undefinedno tienen un "objeto envoltorio", así que nunca tienen acceso a ninguna propiedad u objeto.

👩‍🏫 Por todas estas razones, sí, "todo en JavaScript es un objeto", o quizás sería más acertado decir "todo en JavaScript puede comportarse como un objeto", o incluso "todo en JavaScript puede ser un objeto en cierto momento".

Introducción a las clases en JavaScript

A partir de esta sección vamos a trabajar con un archivo HTML que contenga nada más que un boilerplate y un archivo JS en blanco vinculado al HTML. Lo he llamado sandbox.js.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>OOP</title>
</head>
<body>
    <script src="sandbox.js"></script>
</body>
</html>

Vamos a trabajar en el archivo sandbox.js. Para crear objetos, lo más habitual es crearlos de esta manera:

const user = {
    username: 'Gandalf',
    email: 'gandalf@thewhite.com',
    login() {
        console.log('the user logged in');
    },
    logout() {
        console.log('the user logged out');
    }
};

Para acceder a las propiedades de un objeto, no tenemos más que hacer:

console.log(user.username, user.email); // Gandalf gandalf@thewhite.com

y podremos verlas en nuestra consola. También podemos llamar a alguno de los métodos.

user.login(); // the user logged in

Hasta aquí todo bien, parece que estamos escribiendo un código eficiente y de acuerdo con el principio DRY (Do not Repeat Yourself). ¿Pero qué pasaría si quisiéramos crear otro usuario con las mismas propiedades? 🤔Podríamos copiar y pegar el usuario anterior sustituyendo los valores que necesitásemos. Pero ese código dejaría de ser eficiente y sostenible, porque imaginemos que en lugar de dos usuarios, necesitamos crear 200.

const user2 = {
    username: 'Gimli',
    email: 'gimli@moria.com',
    login() {
        console.log('the user logged in');
    },
    logout() {
        console.log('the user logged out');
    }
};

Para solucionar este problema nacieron las clases de JavaScript. Una clase se construye usando la keyword "new" + un constructor. Seguimos el mismo procedimiento que para construir un object literal, por ejemplo, solo que el constructor "Object" ya viene incluido en JS por defecto.

sintaxis clase JS

Esquema 1

Pero el constructor User debemos crearlo nosotros. (El nombre User también es inventado, podemos darle el nombre que queramos a nuestra clase).

Para llevar a cabo todo esto, podemos tomar dos caminos. El antiguo sería utilizando prototipos (en inglés, prototypes). El moderno, usando clases (en inglés, classes). Las clases fueron introducidas en JS a partir de la versión ES6 del lenguaje, y podemos usarlas utilizando la keyword "class". 

En realidad, las classes son una forma más sencilla de usar los prototypes. Es lo que se conoce como "sintatic sugar". Así que aunque usemos classes, JS lee ese código como si fuera un prototype. 

Una class es una especie de plantilla (en inglés, blueprintque describe qué debe contener un objeto. Por ejemplo, en la vida real, una class sobre un coche debería contener:

propiedades como:

  • color
  • tamaño
  • año de fabricación

funcionalidades como:

  • acelerar
  • frenar
  • encender luces

Todo coche creado a partir de ese blueprint contendría esas propiedades y funcionalidades. Con valores distintos para cada coche, claro. Es decir, un coche podría de ser de color verde, otro rojo, etc. En JS, la construcción de blueprints es muy similar, sólo que las llamamos classes.

Por ejemplo, una User class podría tener estas propiedades y métodos:

{
  username,
  email, 
  login(),
  logout()
}

Así, los valores de esas propiedades y métodos podemos especificarlos cuando creamos un nuevo User object, igual que podíamos darle un valor a un String object cuando lo creábamos.

const userOne = new String('gimli');
const aragorn = new User('Aragorn','theking@gondor.com');
const arwen = new User('Arwen','theelf@rivendel.com');

Veamos todo esto en acción en la siguiente sección.

Constructores de clases

1. Para crear una class, lo primero que debemos hacer es usar la keyword "class" y luego darle nombre a nuestra class y abrir llaves igual que si fuéramos a crear un objeto.

class User {
    
}

Es una convención que el nombre de una class se escriba en mayúscula (la primera letra).

2. Creamos la función constructora (en inglés, la constructor functiondentro de la class. Una constructor function es lo que construye nuestro objeto y establece sus propiedades. Así que cuando en el futuro hagamos:

const userOne = new User();

Ese User() estará llamando a la constructor function del interior de la class.

👉 Una constructor function no deja de ser una función, por tanto, su sintaxis es igual que la de una función. Dentro de la función es donde estableceremos las propiedades de nuestro objeto.

class User {
    constructor() {
        // properties here
    }
}

3. Pero antes, vamos a disipar cualquier duda sobre la keyword "new" aplicada a las classes. Trabajemos con el mismo ejemplo de arriba:

const userOne = new User();

Cada vez que usamos new para crear un nuevo objeto, suceden tres cosas.

1ª) Se crea un nuevo objeto vacío 👉  {  }

2ª) JS vincula el valor de la keyword "this" a ese objeto

3ª) JS llama a la constructor function para construir el objeto

4. Por tanto, ahora podemos crear nuestra primera propiedad y darle algún valor. Si hacemos un console.log de userOne, podremos ver el resultado en la consola.

class User {
    constructor() {
        this.username = 'gandalf';
    }
}

const userOne = new User();

console.log(userOne);
> User {username: "gandalf"}
      username: "gandalf"
      >__proto__: Object

¡Ya hemos creado nuestro primer User object🙌 ¡Yupiii! Ahora ya podemos crear todos los User object que queramos.

const userOne = new User();
const userTwo = new User();

console.log(userOne, userTwo);

Pero, como podrás comprobar, todos son idénticos entre sí. Para construir User object distintos entre sí, no tenemos más que pasarle valores al constructor del User (ver esquema 1)

const userOne = new User('gandalf');
const userTwo = new User('frodo');

Para acceder a esos valores dentro de la constructor function, lo único que tenemos que hacer es pasarle parámetros. En este caso, username. Y con esto ya podemos darle el valor de ese parámetro a this.username.

class User {
    constructor(username) {
        this.username = username;
    }
}

Y ahora en la consola ya deberías ver dos User objects, cada uno con un valor distinto para la propiedad username (Gandalf y Frodo). Así que nuestros User objects son ahora únicos. Pero hay más, porque nuestra constructor function admite todos los parámetros que queramos darle. Por ejemplo, podemos añadir un email como parámetro.

🧐 El orden de los argumentos es muy importante, ya que el primer argumento será asignado al primer parámetro, el segundo argumento, al segundo parámetro, etc.

class User {
    constructor(username, email) {
        this.username = username;
        this.email = email;
    }
}

const userOne = new User('gandalf', 'gandalf@thewhite.com');
const userTwo = new User('frodo', 'frodo@bolson.com');
Al User object creado a partir de una class se le llama instance.

Métodos de las clases

Ya hemos creado nuestros primeros User objects, pero éstos solo tienen propiedades y ningún método. Vamos a cambiar eso. Recuerda que en la constructor function solo podemos añadir propiedades, pero no métodos. Para añadirlos, debemos hacerlo a continuación, fuera de la constructor function. 

1. No estamos añadiendo key-value pairs dentro de la clase User, así que no debemos poner comas entre el constructor y los distintos métodos. Sólo debemos escribirlos uno detrás de otro. Vamos a crear un método llamado login.

class User {
    constructor(username, email) {
        this.username = username;
        this.email = email;
    }
    login() {
        
    }
}

Nuestros métodos deben ser siempre funciones comunes (en inglés, regular functions) porque las arrow functions tienen un comportamiento distinto con respecto a la keyword "this"Básicamente lo que ocurriría si utilizásemos una arrow function, sería que el valor de this estaría vinculado al window object, y no al User object. 😮 

2. Dentro del método login, hacemos un console.log. Después lo llamamos. Podemos crear también otro método llamado logout y llamarlo también.

class User {
    constructor(username, email) {
        this.username = username;
        this.email = email;
    }
    login() {
        console.log(`${this.username} just logged in`);
    }
    logout() {
        console.log(`${this.username} just logged out`);
    }
}

const userOne = new User('gandalf', 'gandalf@thewhite.com');
const userTwo = new User('frodo', 'frodo@bolson.com');

console.log(userOne, userTwo);
userOne.login();
userTwo.logout();

Si vamos a la consola y desplegamos algún User object, verás que tiene especificados todas las propiedades y métodos que hemos definido en la clase.

User {username: "frodo", email: "frodo@bolson.com"}
  email: "frodo@bolson.com"
  username: "frodo"
  __proto__:
      constructor: class User
      login: ƒ login()
      logout: ƒ logout()
      __proto__: Object

 Encadenando métodos

De momento, si queremos llamar a dos métodos de un User object, debemos hacer cada llamada en una línea nueva de código.

userOne.login();
userOne.logout();

Intentar juntar las llamadas en una sola línea nos daría un error.

userOne.login().logout();

La razón por la que esto no funciona es porque el método login no devuelve ningún valor. Cuando no definimos ningún return en un método, JS crea un return automático y le asigna el valor de undefinedAsí, cuando intentamos llamar al método logout, en realidad JS lo está adjuntando el método a undefined, no a userOne. 🙄

Simulación:

fase 1: userOne.login().logout()
fase 2: undefined.logout()
resultado: Uncaught TypeError: Cannot read property 'logout' of undefined

1. Para arreglar esto y conseguir encadenar métodos, vamos a crear otro método llamado increaseScore. Lo único que hará este método es incrementar la puntuación del usuario en +1. Creamos también una propiedad llamada score y le damos el valor de 0.

Todo User tendrá la misma propiedad (score) con el mismo valor inicial (0), por eso no hace falta que se la pasemos por parámetro a la constructor function.

2. Hacemos un console.log dentro de increaseScore para mostrar la puntuación de un User.

3. Llamamos al método increaseScore asociado al userOne, por ejemplo.

class User {
    constructor(username, email) {
        this.username = username;
        this.email = email;
        this.score = 0;
    }
    login() {
        console.log(`${this.username} just logged in`);
    }
    logout() {
        console.log(`${this.username} just logged out`);
    }
    increaseScore() {
        this.score++;
        console.log(`${this.username} has a score of ${this.score}`);
    }
}

const userOne = new User('gandalf', 'gandalf@thewhite.com');
const userTwo = new User('frodo', 'frodo@bolson.com');

console.log(userOne, userTwo);
userOne.increaseScore(); // gandalf has a score of 1

Verás que Gandalf tiene una puntuación de 1. Si invocamos al método otra vez, Gandalf tendrá una puntuación de 2, y así sucesivamente. 

Para poder encadenar métodos, necesitamos hacer un return de algún valor, porque de no hacerlo, ya sabemos que JS devolverá undefined automáticamente. Así que debemos hacer un return del propio objeto (del User instance), porque entonces podremos adjuntarle métodos. Esto se traduce en hacer un return de "this", porque this hace referencia a la instance del User object. 👩‍🏫

class User {
    constructor(username, email) {
        this.username = username;
        this.email = email;
        this.score = 0;
    }
    login() {
        console.log(`${this.username} just logged in`);
        return this;
    }
    logout() {
        console.log(`${this.username} just logged out`);
        return this;
    }
    increaseScore() {
        this.score++;
        console.log(`${this.username} has a score of ${this.score}`);
        return this;
    }
}

Con este cambio, ya podemos encadenar métodos sin problemas.

userOne.login().increaseScore().increaseScore().logout();
userTwo.login().increaseScore().logout();

Si vas a tu consola, deberías ver algo así:

gandalf just logged in
gandalf has a score of 1
gandalf has a score of 2
gandalf just logged out
frodo just logged in
frodo has a score of 1
frodo just logged out

¡Genial! 👌

THE END!

¡Y hasta aquí la primera parte de esta guía sobre Programación Orientada a Objetos! Espero que hayas aprendido algo nuevo 😊.  Si te queda alguna duda, ¡nos vemos en los comentarios!  Si quieres seguir aprendiendo, nos vemos en la parte #2.

Y si crees que este post puede serle útil a alguien, ¡compártelo!

Otros artículos que pueden interesarte

Días del 353 al 386
Objetivos versus realidad Y nuevamente, llegó otro día clave. Llegó…y pasó. El pasado 4 de marzo este Reto Computer Geek[...]
Construye Minioland: Tu primera aplicación con Angular | Parte #1
¿Qué vamos a construir?Minioland, o así he decidido llamar a esta sencilla app, totalmente responsive y de tipo Single Page[...]
Angular: Entendiendo la Directiva ngModel
Angular es un framework que nos permite, entre otras cosas, añadir contenido dinámico a nuestros archivos HTML. Una de las formas[...]
Si crees que este post puede serle útil a alguien, por favor, ¡compártelo!:

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Como toda web legal que se precie, utilizamos cookies para asegurar que damos la mejor experiencia al usuario en nuestro sitio web. Si continúas utilizando este sitio asumiremos que estás de acuerdo. más información

Los ajustes de cookies de esta web están configurados para "permitir cookies" y así ofrecerte la mejor experiencia de navegación posible. Si sigues utilizando esta web sin cambiar tus ajustes de cookies o haces clic en "Aceptar" estarás dando tu consentimiento a esto.

Cerrar