Prototipo en JavaScript: Fundamentos

El concepto de prototipo en JavaScript es uno de los pilares fundamentales del lenguaje. Gracias a él, JavaScript implementa un modelo de herencia basado en objetos en lugar de clases. Este enfoque permite compartir propiedades y métodos entre objetos, optimizando la memoria y promoviendo la reutilización del código.

En este artículo exploraremos cómo funciona el sistema de prototipos en JavaScript, qué beneficios ofrece, y cómo podemos usarlo para escribir código más eficiente y escalable.

¿Qué es el prototipo en JavaScript?

En términos simples, el prototipo es un objeto del cual otro objeto puede heredar propiedades y métodos. Cada objeto en JavaScript tiene una propiedad interna llamada [[Prototype]], que apunta al prototipo de ese objeto. En navegadores modernos, esta propiedad es accesible a través de __proto__ (aunque su uso no es recomendado).

Cuando intentamos acceder a una propiedad de un objeto y este no la tiene, JavaScript busca esa propiedad en su prototipo. Este proceso se denomina cadena prototípica.

Diagrama básico de herencia prototípica:

bash
Objeto -> [[Prototype]] -> Prototipo (Objeto Padre) -> null

¿Cómo funciona la cadena de prototipos?

La cadena de prototipos es una secuencia de objetos que JavaScript sigue al intentar acceder a una propiedad o método. Si no encuentra la propiedad en el objeto original, continúa buscando en su prototipo, y así sucesivamente hasta llegar a null.

Ejemplo básico:

javascript
const animal = {
  tipo: 'mamífero',
  hacerSonido: function() {
    console.log('Sonido genérico');
  }
};

const perro = {
  nombre: 'Fido',
  ladrar: function() {
    console.log('Guau guau');
  }
};

// Estableciendo herencia
perro.__proto__ = animal;

perro.hacerSonido();
console.log(perro.tipo);

En el ejemplo anterior perro hereda del objeto animal. Aunque el objeto perro no tiene una propiedad llamada tipo ni un método llamado hacerSonido(), hereda ambos del prototipo (animal).

Aunque __proto__ es ampliamente soportado, se recomienda usar Object.getPrototypeOf() y Object.setPrototypeOf() para trabajar con prototipos de manera estándar.

javascript
const objetoBase = {
  metodoBase: function() {
    console.log('Método en objeto base');
  }
};

const objetoIntermedio = Object.create(objetoBase);
objetoIntermedio.metodoIntermedio = function() {
  console.log('Método en objeto intermedio');
};

const objetoFinal = Object.create(objetoIntermedio);
objetoFinal.metodoFinal = function() {
  console.log('Método en objeto final');
};

objetoFinal.metodoBase();

Aquí, objetoFinal hereda de objetoIntermedio, que a su vez hereda de objetoBase. Aunque objetoFinal no tiene el método metodoBase(), JavaScript lo encuentra en objetoBase, lo que demuestra el funcionamiento de la cadena de prototipos.

Prototipos con funciones constructoras

Cuando se crea un objeto usando una función constructora, el prototipo del objeto creado será el prototipo del constructor. Este es el lugar ideal para definir métodos que pueden ser compartidos por todas las instancias del objeto.

Ejemplo con función constructora y prototipo:

javascript
function Persona(nombre, edad) {
  this.nombre = nombre;
  this.edad = edad;
}

Persona.prototype.saludar = function() {
  console.log(`Hola, soy ${this.nombre}`);
};

const juan = new Persona('Juan', 25);
const ana = new Persona('Ana', 30);

juan.saludar();
ana.saludar();

Como vemos saludar() está definido en el prototipo de Persona, por lo que todas las instancias creadas con la función constructora Persona comparten el mismo método saludar().

Métodos útiles para trabajar con prototipos

1. Object.getPrototypeOf

El método Object.getPrototypeOf() permite obtener el prototipo de un objeto de manera segura y es la forma recomendada en lugar de acceder directamente a la propiedad __proto__, que es específica de los navegadores y no está estandarizada.

javascript
const gato = {
  nombre: 'Tom',
  maullar: function() {
    console.log('Miau miau');
  }
};

const otroGato = Object.create(gato);
console.log(Object.getPrototypeOf(otroGato) === gato);

Object.getPrototypeOf(otroGato) devuelve el prototipo del objeto otroGato, que en este caso es gato. Es una alternativa recomendada en lugar de acceder directamente a __proto__, lo que garantiza compatibilidad y evita problemas de rendimiento.

2. Object.setPrototypeOf

Puedes cambiar el prototipo de un objeto en tiempo de ejecución usando el método Object.setPrototypeOf(), aunque esta práctica no es recomendada debido al impacto en el rendimiento.

javascript
const ave = {
  volar: function() {
    console.log('Puedo volar');
  }
};

const pez = {
  nadar: function() {
    console.log('Puedo nadar');
  }
};

const pato = {};
Object.setPrototypeOf(pato, ave);
pato.volar(); // "Puedo volar"

Object.setPrototypeOf(pato, pez);
pato.nadar(); // "Puedo nadar"

En el ejemplo anterior el prototipo de pato se cambia primero a ave, permitiéndole volar y luego a pez, permitiéndole nadar.

Cambiar prototipos en tiempo de ejecución puede afectar el rendimiento de la aplicación y hacer que el código sea difícil de depurar. Úsalo con moderación.

Diferencias entre __proto__ y prototype

Es importante no confundir la propiedad __proto__ con prototype. Aunque están relacionadas, tienen propósitos diferentes:

  1. __proto__: Es una propiedad interna de cada objeto que apunta a su prototipo. Es la cadena que sigue JavaScript para encontrar propiedades y métodos heredados.
  2. prototype: Es una propiedad de las funciones constructoras. Define el prototipo que será asignado a los objetos creados por esa función constructora.
javascript
function Animal() {}

console.log(Animal.prototype);
const perro = new Animal();
console.log(perro.__proto__ === Animal.prototype);

Animal.prototype es el prototipo que será asignado a perro.__proto__ cuando se cree una instancia usando la función constructora Animal.

Ventajas del sistema de prototipos

  1. Reutilización de código: Los métodos y propiedades definidos en el prototipo se comparten entre todas las instancias, reduciendo la duplicación y optimizando la memoria.
  2. Eficiencia: Los métodos compartidos en el prototipo no se copian en cada instancia, ahorrando recursos.
  3. Flexibilidad: Permite agregar métodos y propiedades dinámicamente a objetos ya creados.
  4. Herencia: Facilita la creación de jerarquías de objetos y la reutilización de código en proyectos complejos.

Desventajas del sistema de prototipos

  1. Complejidad: La cadena de prototipos puede ser difícil de entender y depurar, especialmente en aplicaciones con jerarquías profundas.
  2. Sobreescritura: La capacidad de sobrescribir métodos compartidos puede introducir errores si no se maneja correctamente.
  3. Compatibilidad: La propiedad __proto__ no es estándar, lo que puede generar problemas en plataformas antiguas.
  4. Curva de aprendizaje: Para desarrolladores acostumbrados a lenguajes basados en clases, como Java o C#, el enfoque de prototipos puede ser confuso.

Conclusión

El sistema de prototipos en JavaScript es una herramienta poderosa que permite implementar herencia y reutilización de código de manera eficiente. Aunque puede parecer complejo al principio, dominarlo es crucial para escribir código escalable y bien estructurado. Con prácticas recomendadas como el uso de Object.getPrototypeOf() y Object.setPrototypeOf(), puedes aprovechar al máximo las ventajas del sistema prototípico de JavaScript.

En el próximo artículo, profundizaremos en cómo combinar el Patrón Constructor/Prototipo para crear objetos más robustos y escalables.

+1
1
+1
0