Desarrollo en el cliente

Juegos en Red - Grado en Diseño y Desarrollo de Videojuegos

Ruben Rodríguez

URJC

Natalia Madrueño

URJC

Mario Manzano

URJC

2026-09-09

URJC Logo DSLab Logo

Orientación a Objetos

Prototipos vs Clases

JavaScript usa herencia prototípica, no clases tradicionales

  • Cualquier objeto puede ser prototipo de otros objetos
  • Los objetos heredan directamente de otros objetos
  • Cadena de herencia flexible y dinámica
  • ES2015+ añadió sintaxis de clases (azúcar sintáctico sobre prototipos)

Nota

A diferencia de Java o C++, JavaScript no tiene clases “reales” en su núcleo

Objeto Literal Simple

La forma más directa de crear objetos

const enemigo = {
    vida: 100,
    damage: 15,
    atacar() {
        return `Enemigo ataca causando ${this.damage} puntos`;
    }
};
  • Definimos propiedades y métodos directamente
  • Sintaxis clara y concisa
  • Ideal para objetos únicos o configuraciones

Creación Basada en Prototipos

Usando Object.create() para heredar

const goblin = Object.create(enemigo);
goblin.vida = 50;
goblin.damage = 8;

console.log(goblin.atacar()); 
// "Enemigo ataca causando 8 puntos"
  • goblin hereda el método atacar() de enemigo
  • Las propiedades propias sobrescriben las heredadas
  • this se refiere al objeto que invoca el método

Herencia con Prototipos - Concepto

Creando cadenas de herencia

  • Cada objeto puede heredar de otro objeto
  • Se forma una cadena de prototipos
  • JavaScript busca propiedades/métodos en la cadena hasta encontrarlos
  • Permite estructurar jerarquías de objetos

Herencia con Prototipos - Base

Creando el objeto base

const personajeBase = {
    mover(x, y) {
        this.x += x;
        this.y += y;
    },
    x:  0,
    y: 0
};
  • Define comportamiento común
  • Puede ser heredado por múltiples objetos
  • Los métodos usan this para acceder a propiedades

Herencia con Prototipos - Extensión

Creando un prototipo intermedio

const prototipoJugador = Object.create(personajeBase);
prototipoJugador.atacar = function(objetivo) {
    return `${this.nombre} ataca a ${objetivo.nombre}`;
};
prototipoJugador.nombre = "Sin nombre";
  • Hereda de personajeBase
  • Añade funcionalidad específica
  • Añade un nombre por defecto.
  • Forma el segundo eslabón de la cadena

Herencia con Prototipos - Instancia

Creando la instancia final

const jugador = Object.create(prototipoJugador);
jugador.nombre = "Aragorn";
jugador.x = 10;
jugador.y = 10;
jugador.puntuacion = 3;
  • Tiene acceso a mover() y atacar()
  • Define sus propias propiedades
  • Propiedades que ocultan las del prototipo (locales) y nuevas.
  • Completa la cadena de herencia

Visión Completa

Base

let personajeBase = {
    mover(x, y) {
        this.x += x;
        this.y += y;
    },
    x:  0,
    y: 0
};

Prototipo

let prototipoJugador = Object.create(personajeBase);
prototipoJugador.atacar = function(objetivo) {
    return `${this.nombre} ataca a ${objetivo.nombre}`;
};
prototipoJugador.nombre = "Sin nombre";

Instancia

let jugador = Object.create(prototipoJugador);
  • Acceder a prototipoJugador.nombre y a jugador.nombre
  • añadir los cambios a nivel de instancia y ejecutar de nuevo.
  • Comprobar las propiedades con Object.keys(INSTANCIA) antes y después.
jugador.nombre = "Aragorn";
jugador.x = 10;
jugador.y = 10;
jugador.puntuacion = 3;

Acceso a Propiedades

Notación punto

La forma más común y legible

const config = {
    sonido: true,
    volumen: 0.8,
    idioma: "es"
};

console.log(config.sonido);
config.volumen = 0.5;
  • Sintaxis clara: objeto.propiedad
  • Recomendada cuando el nombre es conocido
  • No funciona con nombres dinámicos

Notación corchetes

Para propiedades dinámicas o especiales

console.log(config["idioma"]);

const propiedad = "volumen";
console.log(config[propiedad]);
  • Permite nombres de propiedad dinámicos
  • Útil con variables o bucles
  • Necesaria para nombres con espacios o caracteres especiales

Añadir Propiedades Dinámicamente

JavaScript permite modificar objetos en tiempo de ejecución

config.dificultad = "normal";
config["nivel-maximo"] = 10;
  • Se pueden añadir propiedades después de crear el objeto
  • Característica de la naturaleza dinámica de JavaScript
  • Útil pero puede complicar el seguimiento del código

Iteración sobre Propiedades - For…in

Recorriendo todas las propiedades

const inventario = {
    espada: 1,
    pocion: 5,
    oro: 150
};

for (let item in inventario) {
    console.log(`${item}: ${inventario[item]}`);
}
  • Itera sobre propiedades enumerables
  • Incluye propiedades heredadas del prototipo
  • La variable toma el nombre de cada propiedad

Iteración sobre Propiedades - Verificación

Comprobando existencia de propiedades

if ("oro" in inventario) {
    console.log("El jugador tiene oro");
}

if (inventario.hasOwnProperty("espada")) {
    console.log("Propiedad propia, no heredada");
}
  • in verifica si existe la propiedad (propia o heredada)
  • hasOwnProperty() verifica solo propiedades propias
  • No necesitamos acceder al valor para verificar

Iteración sobre Propiedades - Arrays

Obteniendo claves y valores como arrays

const items = Object.keys(inventario);
// ["espada", "pocion", "oro"]

const cantidades = Object.values(inventario);
// [1, 5, 150]

const pares = Object.entries(inventario);
// [["espada", 1], ["pocion", 5], ["oro", 150]]
  • Útil para trabajar con métodos de array (map, filter, etc.)
  • Solo devuelven propiedades propias enumerables

Función Constructor - Definición

El patrón clásico pre-ES2015

function Jugador(nombre, x, y) {
    this.nombre = nombre;
    this.x = x;
    this.y = y;
    this.vida = 100;
}
  • Se invoca con new para crear instancias
  • this se refiere al nuevo objeto creado
  • Inicializa las propiedades de la instancia
  • Por convención, nombre en PascalCase

Función Constructor - Métodos

Añadiendo métodos al prototipo

Jugador.prototype.mover = function(deltaX, deltaY) {
    this.x += deltaX;
    this.y += deltaY;
};

Jugador.prototype.mostrarPosicion = function() {
    return `${this.nombre} está en (${this.x}, ${this.y})`;
};
  • Los métodos van en el prototipo, no en el constructor
  • Se comparten entre todas las instancias (ahorra memoria)
  • Tienen acceso a this de la instancia

Función Constructor - Uso

Creando instancias

const player1 = new Jugador("Aragorn", 10, 20);
const player2 = new Jugador("Legolas", 15, 25);

player1.mover(2, 3);
console.log(player1.mostrarPosicion());
// "Aragorn está en (12, 23)"
  • Siempre usar new para invocar constructores
  • Cada instancia tiene sus propias propiedades
  • Los métodos son compartidos vía prototipo

Herencia con Constructor - Clase Base

Definiendo el constructor padre

function Personaje(nombre, vida) {
    this.nombre = nombre;
    this.vida = vida;
}

Personaje.prototype.saludar = function() {
    return `Hola, soy ${this.nombre}`;
};
  • Constructor base con propiedades comunes
  • Métodos compartidos en el prototipo

Herencia con Constructor - Clase Hija

Extendiendo la funcionalidad

function Guerrero(nombre, vida, fuerza) {
    Personaje.call(this, nombre, vida);
    this.fuerza = fuerza;
}
  • Personaje.call(this, ...) llama al constructor padre
  • Inicializa las propiedades heredadas
  • Añade propiedades específicas de Guerrero

Herencia con Constructor - Cadena de Prototipos

Estableciendo la herencia correctamente

Guerrero.prototype = Object.create(Personaje.prototype);
Guerrero.prototype.constructor = Guerrero;

Guerrero.prototype.atacar = function() {
    return `${this.nombre} ataca con fuerza ${this.fuerza}`;
};
  • Object.create() establece el prototipo correcto
  • Restauramos la propiedad constructor
  • Ahora podemos añadir métodos específicos

Herencia con Constructor - Resultado

Usando la herencia completa

const conan = new Guerrero("Conan", 150, 25);

console.log(conan.saludar());
// "Hola, soy Conan" (heredado de Personaje)

console.log(conan.atacar());
// "Conan ataca con fuerza 25" (propio de Guerrero)
  • conan tiene acceso a métodos de ambos constructores
  • Patrón verboso y propenso a errores

Clases ES2015+ - Introducción

Sintaxis moderna y clara

  • Introducida en ES2015 (ES6)
  • Azúcar sintáctico sobre prototipos
  • Más familiar para desarrolladores de otros lenguajes OO
  • Sintaxis más estructurada y menos propensa a errores

Clases ES2015+ - Definición Básica

Declarando una clase

class GameObject {
    constructor(x, y) {
        this.x = x;
        this.y = y;
        this.activo = true;
    }
}
  • Palabra clave class seguida del nombre
  • Método constructor() se ejecuta al crear instancias
  • Inicializa las propiedades del objeto

Clases ES2015+ - Métodos

Añadiendo comportamiento

class GameObject {
    constructor(x, y) {
        this.x = x;
        this.y = y;
        this.activo = true;
    }

    actualizar(deltaTime) {
        if (!this.activo) return;
        // Lógica de actualización
    }

    destruir() {
        this.activo = false;
    }
}
  • Los métodos se definen dentro de la clase
  • No se usa function keyword
  • Automáticamente se añaden al prototipo

Herencia con Clases - Extends

Creando subclases

class Enemigo extends GameObject {
    constructor(x, y, tipo) {
        super(x, y);  // Llamada al constructor padre
        this.tipo = tipo;
        this.vida = 50;
        this.velocidad = 2;
    }
}
  • extends establece la herencia
  • super() llama al constructor de la clase padre
  • Debe llamarse antes de usar this en el constructor hijo

Herencia con Clases - Sobrescritura

Extendiendo métodos heredados

class Enemigo extends GameObject {
    // ... constructor ...

    actualizar(deltaTime) {
        super.actualizar(deltaTime);
        if (this.vida > 0) {
            this.x += this.velocidad;
        }
    }
}
  • Podemos sobrescribir métodos del padre
  • super.nombreMetodo() llama a la versión del padre
  • Permite extender sin reemplazar completamente

Getters y Setters - Concepto

Propiedades calculadas y validación

  • Getters: propiedades de solo lectura o calculadas
  • Setters: validación al asignar valores
  • Se usan como propiedades normales (sin paréntesis)
  • Permiten encapsular lógica de acceso

Getters y Setters - Getter

Propiedades de solo lectura

class Enemigo extends GameObject {
    constructor(x, y, tipo) {
        super(x, y);
        this._vida = 50;
    }

    get estaVivo() {
        return this._vida > 0;
    }
}

const orc = new Enemigo(10, 20, "Orc");
if (orc.estaVivo) { // Sin paréntesis
    console.log("El enemigo sigue vivo");
}
  • Palabra clave get antes del nombre
  • Se accede como propiedad, no como método
  • Útil para valores derivados

Getters y Setters - Setter

Validación al asignar

class Enemigo extends GameObject {
    // ... constructor ...

    set vida(valor) {
        this._vida = Math.max(0, valor);
        if (this._vida === 0) {
            this.destruir();
        }
    }

    get vida() {
        return this._vida;
    }
}
  • Palabra clave set antes del nombre
  • Permite validar y ejecutar lógica adicional
  • Por convención, la propiedad real usa _ como prefijo

Getters y Setters - Uso

Asignación transparente

const goblin = new Enemigo(5, 10, "Goblin");

goblin.vida = 30;      // Usa el setter
console.log(goblin.vida); // Usa el getter

goblin.vida = -10;     // Setter lo convierte a 0
// El setter también llama a destruir()
  • Sintaxis de propiedad normal
  • El setter ejecuta validación automáticamente
  • Transparente para el código que usa la clase

Métodos Estáticos - Concepto

Métodos de la clase, no de instancias

  • Pertenecen a la clase, no a objetos individuales
  • Se invocan sobre la clase misma
  • No tienen acceso a this de instancia
  • Útiles para funciones de utilidad o métodos factoría

Métodos Estáticos - Definición

Palabra clave static

class Enemigo extends GameObject {
    // ... resto del código ...

    static crearOrc() {
        const orc = new Enemigo(0, 0, "Orc");
        orc.vida = 80;
        orc.velocidad = 1.5;
        return orc;
    }

    static crearGoblin() {
        const goblin = new Enemigo(0, 0, "Goblin");
        goblin.vida = 30;
        goblin.velocidad = 3;
        return goblin;
    }
}
  • Prefijo static antes del método
  • Patrón factoría para crear instancias preconfiguradas

Métodos Estáticos - Uso

Invocación sobre la clase

const orc = Enemigo.crearOrc();
const goblin = Enemigo.crearGoblin();

// NO se puede llamar sobre instancias
const enemigo = new Enemigo(5, 5, "Slime");
// enemigo.crearOrc(); // ❌ Error
  • Se llaman con NombreClase.metodoEstatico()
  • No están disponibles en las instancias
  • Similares a métodos de clase en Java/C++

Uso de Clases - Creación

Instanciando objetos

const goblin = new Enemigo(10, 20, "Goblin");
goblin.vida = 30;

const orc = Enemigo.crearOrc(); // Método estático
  • Constructor normal con new
  • Podemos usar setters para asignar valores
  • Los métodos estáticos crean instancias preconfiguradas

Uso de Clases - Polimorfismo

Tratamiento uniforme de objetos diferentes

const enemigos = [goblin, orc];

enemigos.forEach(enemigo => {
    enemigo.actualizar(16);
    if (enemigo.estaVivo) {
        enemigo.mover(1, 0);
    }
});
  • Todos los enemigos comparten la misma interfaz
  • Podemos tratarlos de forma uniforme
  • Cada uno puede tener comportamiento específico

Ventajas de Clases ES2015+

Por qué usar la sintaxis moderna

  • Sintaxis más clara: Familiar para otros lenguajes OO
  • Herencia simplificada: extends y super() más directos
  • Menos errores: Estructura más rígida previene problemas comunes
  • Getters y setters integrados: Encapsulación natural
  • Métodos estáticos: Organización clara de funciones de utilidad
  • Mejor soporte de herramientas: IDEs y linters funcionan mejor

Tip

La sintaxis de clases es la recomendada para proyectos nuevos

Resumen: Prototipos vs Clases

Dos formas, mismo resultado

Aspecto Prototipos Clases ES2015+
Sintaxis Verbosa y manual Clara y estructurada
Herencia Object.create() y call() extends y super()
Métodos Funcion.prototype.metodo Dentro de la clase
Uso actual Legacy code Recomendado

Importante

Las clases son azúcar sintáctico: por debajo siguen usando prototipos