Rutas
/
JavaScript
JavaScript
/
Ejercicios
/
Sistema de ventanas
Sistema de ventanas

Sistema de ventanas

Ejercicio de aprendizaje

Introducción

JavaScript incluye las capacidades para la programación orientada a objetos (POO). En la POO, quieres crear objetos (instancias) a partir de «plantillas» (clases) para que incluyan ciertos datos y cierta funcionalidad. Las propiedades de datos se llaman campos en el contexto de la POO, y las propiedades de función se llaman métodos.

JavaScript no tenía clases en absoluto antes de que se añadieran a la especificación del lenguaje en 2015, pero permitía la programación orientada a objetos mediante la herencia basada en prototipos. Y aunque hoy en día disponemos de la palabra clave class, JavaScript sigue siendo un lenguaje basado en prototipos.

Para entender qué significa ser un lenguaje basado en prototipos y cómo funciona realmente JavaScript, volveremos a la época en la que no existían las clases.

Sintaxis de prototipos

Función constructora

En JavaScript, la plantilla (clase) se crea con una función normal. Cuando una función se va a usar como plantilla de este tipo, se denomina función constructora y la convención es que el nombre de la función empiece con mayúscula. Las instancias (objetos) se derivan de la plantilla usando la palabra clave new al invocar la función constructora.

function Car() {
  // ...
}

const myCar = new Car();
const yourCar = new Car();

Es importante señalar que, en JavaScript, las instancias y la función constructora mantienen una relación entre sí incluso después de que se hayan creado las instancias. Cada objeto instancia incluye una propiedad interna oculta a la que la especificación del lenguaje se refiere como [[prototype]]. Contiene una referencia al valor de la clave prototype de la función constructora. Sí, has leído bien: una función de JavaScript puede tener pares clave/valor porque, en realidad, también es un objeto.

Note

En resumen:

  • Los constructores en JavaScript son funciones normales.
  • Construir una nueva instancia crea un objeto con una relación con su constructor denominada su prototipo.
  • Las funciones son objetos (objetos invocables) y, por tanto, pueden tener propiedades.
  • La propiedad prototype de la función constructora se convertirá en el prototipo de la instancia.

Campos de instancia

A menudo quieres que todos los objetos derivados (instancias) incluyan algunos campos y pasarles algunos valores iniciales cuando se construye el objeto. Esto se puede hacer con la palabra clave this. Dentro de la función constructora, this representa el nuevo objeto que se creará con new. this se devuelve automáticamente desde la función constructora cuando se llama con new.

Eso significa que podemos añadir campos a la nueva instancia añadiéndolos a this en la función constructora.

function Car(color, weight) {
  this.color = color;
  this.weight = weight;
  this.engineRunning = false;
}

const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false

Métodos de instancia

Los métodos se añaden a través de la propiedad prototype de la función constructora. Dentro de un método, puedes acceder a los campos de la instancia a través de this. Esto funciona gracias a la siguiente regla general.

Cuando se llama a una función como método de un objeto, su this se establece en el objeto sobre el que se llama al método. 1

function Car() {
  this.engineRunning = false;
  // ...
}

Car.prototype.startEngine = function () {
  this.engineRunning = true;
};

Car.prototype.addGas = function (litre) {
  // ...
};

const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true

La cadena de prototipos

myCar en el ejemplo anterior es un objeto normal de JavaScript y, si lo inspeccionáramos (por ejemplo, en la consola del navegador), no encontraríamos una propiedad startEngine con una función como valor directamente dentro del objeto myCar. Entonces, ¿cómo funciona el código anterior?

El secreto aquí se llama cadena de prototipos. Cuando intentas acceder a cualquier propiedad (campo o método) de un objeto, JavaScript comprueba primero si la clave correspondiente se encuentra directamente en el propio objeto. Si no es así, continúa buscando la clave en el objeto al que hace referencia la propiedad [[prototype]] del objeto original. Como se ha mencionado antes, en nuestro ejemplo [[prototype]] apunta a la propiedad prototype de la función constructora. Ahí es donde JavaScript encontraría la función startEngine, porque la añadimos allí.

function Car() {
  // ...
}

Car.prototype.startEngine = function () {
  // ...
};

Y la cadena no termina ahí. La propiedad [[prototype]] de Car.prototype (myCar.[[prototype]].[[prototype]]) hace referencia a Object.prototype (la propiedad prototype de la función constructora Object). Contiene métodos generales que están disponibles para todos los objetos de JavaScript, por ejemplo toString(). En conclusión, puedes llamar a myCar.toString() y ese método existirá porque JavaScript busca ese método en toda la cadena de prototipos.

Caution

Ten en cuenta que la cadena de prototipos solo se recorre al obtener un valor. Establecer una propiedad directamente o eliminar una propiedad de un objeto instancia solo afecta a esa instancia concreta. Es posible que no sea lo que esperas si estás acostumbrado a un lenguaje con herencia basada en clases.

Sintaxis de clases

Hoy en día, JavaScript admite definir clases con la palabra clave class. Esto se añadió a la especificación del lenguaje en 2015. Por un lado, esto proporciona azúcar sintáctico que hace que las clases sean más fáciles de leer y escribir. La nueva sintaxis se parece más a cómo se escriben las clases en lenguajes como C++ o Java. Los desarrolladores que vienen de esos lenguajes se adaptan con más facilidad. Por otro lado, la sintaxis de clases allana el camino para nuevas funcionalidades del lenguaje que no están disponibles en la sintaxis de prototipos.

Declaraciones de clase

Con la nueva sintaxis, las clases se definen con la palabra clave class, seguida del nombre de la clase y del cuerpo de la clase entre llaves. El cuerpo contiene la definición de la función constructora, es decir, un método especial con el nombre constructor. Esta función funciona igual que la función constructora de la sintaxis de prototipos. El cuerpo de la clase también contiene todas las definiciones de métodos. La sintaxis de los métodos es similar a la notación abreviada que vimos para añadir funciones como valores dentro de un objeto; consulta el concepto Objetos.

class Car {
  constructor(color, weight) {
    this.color = color;
    this.weight = weight;
    this.engineRunning = false;
  }

  startEngine() {
    this.engineRunning = true;
  }

  addGas(litre) {
    // ...
  }
}

const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true

Ten en cuenta que, en realidad, JavaScript sigue siendo un lenguaje basado en prototipos. Todos los mecanismos que aprendimos en la sección «Sintaxis de prototipos» anterior siguen aplicándose.

Campos privados, getters y setters

De forma predeterminada, todos los campos de instancia son públicos en JavaScript. Se puede acceder a ellos y asignarles valores directamente.

Sin embargo, existe una convención establecida de que los campos y métodos que empiezan por guion bajo deben tratarse como privados. Nunca deben ser accesibles directamente desde fuera de la clase.

Los campos privados a veces van acompañados de getters y setters. Con las palabras clave get y set puedes definir funciones que se ejecutan cuando se accede a una propiedad con el mismo nombre que la función o se le asigna un valor.

class Car {
  constructor() {
    this._mileage = 0;
  }

  get mileage() {
    return this._mileage;
  }

  set mileage(value) {
    throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
    // Just an example, usually you would not provide a setter in this case.
  }
}

const myCar = new Car();
myCar.mileage;
// => 0
myCar.mileage = 100;
// => Error: Mileage cannot be manipulated, 100 is ignored.

  1. Ejemplos de this: como método de objeto, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (consultado el 3 de diciembre de 2021) ↩

Instrucciones

En este ejercicio vas a simular un sistema informático basado en ventanas. Crearás algunas ventanas que se pueden mover y redimensionar. La siguiente imagen es representativa de los valores con los que trabajarás a continuación.

                  <--------------------- screenSize.width --------------------->

       ^          ┌────────────────────────────────────────────────────────────┐
       |          │                                                            │
       |          │         position.x, _                                      │
       |          │         position.y   \                                     │
       |          │                       \<----- size.width ----->            │
       |          │                 ^      *──────────────────────┐            │
       |          │                 |      │        title         │            │
       |          │                 |      ├──────────────────────┤            │
screenSize.height │                 |      │                      │            │
       |          │            size.height │                      │            │
       |          │                 |      │       contents       │            │
       |          │                 |      │                      │            │
       |          │                 |      │                      │            │
       |          │                 v      └──────────────────────┘            │
       |          │                                                            │
       |          │                                                            │
       v          └────────────────────────────────────────────────────────────┘

📣 Para practicar tu amplia variedad de habilidades con JavaScript, intenta resolver las tareas 1 y 2 con sintaxis de prototipos y las tareas restantes con sintaxis de clases.

1. Define Size para almacenar las dimensiones de la ventana

Define una clase (función constructora) llamada Size. Debe tener dos campos, width y height, que almacenan las dimensiones actuales de la ventana. La función constructora debe aceptar valores iniciales para estos campos. La anchura se proporciona como primer parámetro y la altura como segundo. La anchura y la altura predeterminadas deben ser 80 y 60, respectivamente.

Además, define un método resize(newWidth, newHeight) que recibe una nueva anchura y altura como parámetros y cambia los campos para reflejar el nuevo tamaño.

const size = new Size(1080, 764);
size.width;
// => 1080
size.height;
// => 764

size.resize(1920, 1080);
size.width;
// => 1920
size.height;
// => 1080

2. Define Position para almacenar la posición de una ventana

Define una clase (función constructora) llamada Position con dos campos, x e y, que almacenan la posición horizontal y vertical actuales, respectivamente, de la esquina superior izquierda de la ventana. La función constructora debe aceptar valores iniciales para estos campos. El valor de x se proporciona como primer parámetro y el de y como segundo. El valor predeterminado debe ser 0 para ambos campos.

La posición (0, 0) es la esquina superior izquierda de la pantalla, con los valores de x que aumentan a medida que te mueves hacia la derecha y los valores de y que aumentan a medida que te mueves hacia abajo.

Define también un método move(newX, newY) que recibe nuevos parámetros x e y y cambia las propiedades para reflejar la nueva posición.

const point = new Position();
point.x;
// => 0
point.y;
// => 0

point.move(100, 200);
point.x;
// => 100
point.y;
// => 200

3. Define una clase ProgramWindow

Define una clase ProgramWindow con los siguientes campos:

  • screenSize: contiene un valor fijo de tipo Size con width 800 y height 600
  • size : contiene un valor de tipo Size; el valor inicial es el valor predeterminado de la instancia de Size
  • position : contiene un valor de tipo Position; el valor inicial es el valor predeterminado de la instancia de Position

Cuando se abre (crea) la ventana, siempre tiene el tamaño y la posición predeterminados al principio.

const programWindow = new ProgramWindow();
programWindow.screenSize.width;
// => 800

// Similar for the other fields.

Nota: se usa el nombre ProgramWindow en lugar de Window para diferenciar la clase de la clase Window integrada que existe en los entornos de navegador.

4. Añade un método para redimensionar la ventana

La clase ProgramWindow debe incluir un método resize. Debe aceptar como entrada un parámetro de tipo Size e intentar redimensionar la ventana al tamaño especificado.

Sin embargo, el nuevo tamaño no puede superar ciertos límites.

  • La altura o la anchura mínimas permitidas son 1. Las alturas o anchuras solicitadas menores que 1 se recortarán a 1.
  • La altura y la anchura máximas dependen de la posición actual de la ventana; los bordes de la ventana no pueden superar los bordes de la pantalla. Los valores mayores que estos límites se recortarán al mayor tamaño que puedan tomar. Por ejemplo, si la posición de la ventana está en x = 400, y = 300 y se solicita un redimensionado a height = 400, width = 300, la ventana se redimensionaría a height = 300, width = 300, ya que la pantalla no es lo suficientemente grande en la dirección y para acomodar por completo la solicitud.
const programWindow = new ProgramWindow();

const newSize = new Size(600, 400);
programWindow.resize(newSize);
programWindow.size.width;
// => 600
programWindow.size.height;
// => 400

5. Añade un método para mover la ventana

Además de la funcionalidad de redimensionado, la clase ProgramWindow también debe incluir un método move. Debe aceptar como entrada un parámetro de tipo Position. El método move es similar a resize; sin embargo, este método ajusta la posición de la ventana al valor solicitado, en lugar del tamaño.

Al igual que con resize, la nueva posición no puede superar ciertos límites.

  • La posición más pequeña es 0 tanto para x como para y.
  • La posición máxima en cualquier dirección depende del tamaño actual de la ventana. Los bordes no pueden superar los bordes de la pantalla. Los valores mayores que estos límites se recortarán al mayor tamaño que puedan tomar. Por ejemplo, si el tamaño de la ventana está en x = 250, y = 100 y se solicita un movimiento a x = 600, y = 200, la ventana se movería a x = 550, y = 200, ya que la pantalla no es lo suficientemente grande en la dirección x para acomodar por completo la solicitud.
const programWindow = new ProgramWindow();

const newPosition = new Position(50, 100);
programWindow.move(newPosition);
programWindow.position.x;
// => 50
programWindow.position.y;
// => 100

6. Cambia una ventana de programa

Implementa una función changeWindow que acepta como entrada una instancia de ProgramWindow y cambia la ventana al tamaño y la posición especificados. La función debe devolver la instancia de ProgramWindow que se pasó después de aplicar los cambios.

La ventana debe tener una anchura de 400, una altura de 300 y estar situada en x = 100, y = 150.

const programWindow = new ProgramWindow();
changeWindow(programWindow);
programWindow.size.width;
// => 400

// Similar for the other fields.
Editar en GitHub El enlace se abre en una ventana o pestaña nueva
JavaScript Exercism

¿Listo para empezar Sistema de ventanas?

Regístrate en Exercism para aprender y dominar JavaScript con 37 conceptos159 ejercicios y mentoría humana real, todo gratis.