Tracks
/
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 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 agregaran 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 existe la palabra clave class, JavaScript sigue siendo un lenguaje basado en prototipos.

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

Sintaxis de prototipos

Función constructora

En JavaScript, la plantilla (la clase) se implementa con una función normal. Cuando una función se va a usar como una plantilla de este tipo, se llama 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 notar que en JavaScript, las instancias y la función constructora mantienen una relación entre sí incluso después de que se crearon las instancias. Cada objeto de instancia incluye una propiedad interna y 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í, leíste bien: una función de JavaScript puede tener pares clave/valor porque, tras bambalinas, 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 llamada su prototipo.
  • Las funciones son objetos (objetos invocables) y, por lo tanto, pueden tener propiedades.
  • La propiedad prototype del constructor (de la función) se convertirá en el prototipo de la instancia.

Campos de instancia

A menudo quieres que todos los objetos derivados (instancias) incluyan algunos campos y pasar algunos valores iniciales para ellos cuando se construye el objeto. Esto se puede lograr 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 agregar campos a la nueva instancia agregá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 agregan 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 por 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 la cadena de prototipos. Cuando intentas acceder a cualquier propiedad (campo o método) de un objeto, JavaScript primero verifica si la clave correspondiente se puede encontrar directamente en el objeto mismo. Si no, continúa buscando la clave en el objeto al que hace referencia la propiedad [[prototype]] del objeto original. Como se mencionó 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 agregamos ahí.

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 a lo largo de toda la cadena de prototipos.

Caution

Ten en cuenta que la cadena de prototipos solo se recorre cuando se obtiene un valor. Establecer una propiedad directamente o eliminar una propiedad de un objeto de instancia solo afecta a esa instancia específica. Esto podría no ser lo que esperas si estás acostumbrado a un lenguaje con herencia basada en clases.

Sintaxis de clases

Hoy en día, JavaScript permite definir clases con la palabra clave class. Esto se agregó a la especificación del lenguaje en 2015. Por un lado, esto proporciona azúcar sintáctica 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. Quienes 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 clases

Con la nueva sintaxis, las clases se definen con la palabra clave class, seguida del nombre de la clase y el 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 agregar 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, tras bambalinas, 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 manera 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 con un guion bajo deben tratarse como privados. Nunca se debe acceder a ellos 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 un 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 de computadora basado en ventanas. Vas a crear algunas ventanas que se pueden mover y cambiar de tamaño. La siguiente imagen es representativa de los valores con los que vas a trabajar a continuación.

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

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

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

1. Define Size para guardar 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. El ancho se proporciona como primer parámetro y la altura como segundo. El ancho y la altura predeterminados deben ser 80 y 60, respectivamente.

Además, define un método resize(newWidth, newHeight) que reciba un nuevo ancho y una nueva altura como parámetros y cambie 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 guardar la posición de una ventana

Define una clase (función constructora) llamada Position con dos campos, x y 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 valor 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, donde los valores de x aumentan a medida que te mueves a la derecha y los valores de y aumentan a medida que te mueves hacia abajo.

También define un método move(newX, newY) que reciba nuevos parámetros x e y y cambie 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 aparte: el nombre ProgramWindow se usa en lugar de Window para diferenciar la clase de la clase Window integrada que existe en los entornos de navegador.

4. Agrega un método para cambiar el tamaño de la ventana

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

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

  • La altura o el ancho mínimo permitido es 1. Las alturas o los anchos solicitados menores que 1 se recortarán a 1.
  • La altura y el ancho máximos dependen de la posición actual de la ventana; los bordes de la ventana no pueden sobrepasar los bordes de la pantalla. Los valores mayores que estos límites se recortarán al mayor tamaño que puedan alcanzar. Por ejemplo, si la posición de la ventana es x = 400, y = 300 y se solicita un cambio de tamaño a height = 400, width = 300, entonces la ventana se cambiaría a height = 300, width = 300, ya que la pantalla no es lo suficientemente grande en la dirección y para acomodar completamente la solicitud.
const programWindow = new ProgramWindow();

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

5. Agrega un método para mover la ventana

Además de la funcionalidad de cambiar el tamaño, la clase ProgramWindow también debe incluir un método move. Debe aceptar como argumento 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 exceder 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 sobrepasar los bordes de la pantalla. Los valores mayores que estos límites se recortarán al mayor tamaño que puedan alcanzar. 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, entonces 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 completamente 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 acepte como argumento una instancia de ProgramWindow y cambie 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 un ancho de 400, una altura de 300 y estar posicionada 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 una pestaña nuevas
JavaScript Exercism

¿Todo 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.