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. En el contexto de la POO, las propiedades de datos se llaman campos 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 existe 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 JavaScript en realidad, retrocedamos a la época en la que no había clases.
En JavaScript, la plantilla (la clase) se implementa mediante una función normal.
Cuando una función se va a usar como plantilla de este tipo, se denomina función constructora y, por convención, el nombre de la función debe empezar por 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 creen las instancias.
Cada objeto 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í, has leído bien: una función de JavaScript puede tener pares clave/valor porque, en realidad, también es un objeto.
Desde 2015, se puede acceder a [[prototype]] mediante Object.getPrototypeOf().
Antes de eso, era accesible mediante la clave __proto__ en muchos entornos.
No confundas el prototipo de un objeto ([[prototype]]) con la propiedad prototype de la función constructora.
Para resumir:
prototype de la función constructora se convertirá en el prototipo de la instancia.A menudo quieres que todos los objetos derivados (instancias) incluyan ciertos campos y que se les pasen algunos valores iniciales para esos campos cuando se construye el objeto.
Esto se puede conseguir mediante la palabra clave this.
Dentro de la función constructora, this representa el nuevo objeto que se creará mediante new.
La función constructora devuelve this automáticamente cuando se la llama con new.
Eso significa que podemos añadir campos a la nueva instancia añadiéndolos a this dentro de 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
Los métodos se añaden mediante la propiedad prototype de la función constructora.
Dentro de un método, puedes acceder a los campos de la instancia mediante this.
Esto funciona gracias a la siguiente regla general.
Cuando se llama a una función como método de un objeto, su
thisse 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
myCar en el ejemplo anterior es un objeto de JavaScript normal y, si lo inspeccionáramos (por ejemplo, en la consola del navegador), no encontraríamos directamente dentro del objeto myCar una propiedad startEngine con una función como valor.
Entonces, ¿cómo funciona siquiera 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 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().
El [[prototype]] de Object suele ser null, así que la cadena de prototipos termina ahí.
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.
Puedes encontrar un ejemplo detallado en el artículo de MDN «Inheritance and the prototype chain».
Ten en cuenta que la cadena de prototipos solo se recorre cuando se recupera un valor. Establecer una propiedad directamente o eliminar una propiedad de un objeto instancia solo afecta a esa instancia concreta. Puede que esto no sea lo que esperas si estás acostumbrado a un lenguaje con herencia basada en clases.
JavaScript te permite añadir métodos a todas las instancias existentes incluso después de haberlas creado.
Hemos aprendido que cada instancia mantiene una referencia a la propiedad prototype de la función constructora.
Eso significa que, si añades una entrada a ese objeto prototype, esa nueva entrada (por ejemplo, un nuevo método) estará disponible de inmediato para todas las instancias creadas a partir de esa función constructora.
function Car() {
this.engineRunning = false;
}
const myCar = new Car();
// Calling myCar.startEngine() here would result in "TypeError:
// myCar.startEngine is not a function".
Car.prototype.startEngine = function () {
this.engineRunning = true;
};
myCar.startEngine();
// This works, even though myCar was created before the method
// was added.
En teoría, los métodos dinámicos pueden incluso usarse para ampliar la funcionalidad de objetos integrados como Object o Array modificando su prototipo.
Esto se llama monkey patching.
Como este cambio afecta a toda la aplicación, conviene evitarlo para prevenir efectos secundarios no deseados.
El único caso de uso razonable es proporcionar un polyfill para un método que falta en entornos más antiguos.
Hoy en día, JavaScript permite 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.
A los desarrolladores que vienen de esos lenguajes les resulta más fácil adaptarse.
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.
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
De forma similar a las declaraciones de función y a las expresiones de función, JavaScript también admite expresiones de clase además de la declaración de clase que se muestra arriba.
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 siendo válidos.
De forma predeterminada, todos los campos de instancia son públicos en JavaScript. Se puede acceder a ellos directamente y asignarles valores.
Añadir campos privados reales a la especificación del lenguaje es un trabajo en curso; consulta el documento de propuesta para más detalles.
Mientras tanto, puedes hacer uso de la convención establecida de que los campos y métodos que empiezan por 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.
En la POO, a veces quieres proporcionar campos o métodos de utilidad que no dependen de la instancia concreta.
En su lugar, se definen para la propia clase.
Esto se consigue con la palabra clave static.
class Car {
static type = 'vehicle';
static isType(targetType) {
return targetType === 'vehicle';
}
}
Car.type;
// => 'vehicle'
Car.isType('road sign');
// => false
Además del tipo de herencia a lo largo de la cadena de prototipos que vimos antes, también puedes representar la herencia entre clases en JavaScript. Esto se trata en el concepto Herencia.
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) ↩