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 funciones 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 ser un lenguaje basado en prototipos y cómo funciona JavaScript en realidad, volveremos a la época en la que no había clases.
En JavaScript, la plantilla (la clase) se implementa mediante una función normal.
Cuando se supone que una función se usará como esa plantilla, se le 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 instancia incluye una propiedad interna oculta a la que se hace referencia como [[prototype]] en la especificación del lenguaje.
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 también es un objeto detrás de escena.
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.
En resumen:
prototype del constructor (la función) se convertirá en el prototipo de la instancia.A menudo quieres que todos los objetos derivados (instancias) incluyan algunos campos y pasar algunos valores iniciales para esos campos cuando se construye el objeto.
Esto se puede lograr mediante la palabra clave this.
Dentro de la función constructora, this representa el nuevo objeto que se creará mediante 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
Los métodos se agregan 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 el 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 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 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 primero comprueba si la clave correspondiente se encuentra directamente en el propio objeto.
Si no es así, sigue 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 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 específica. Esto podría no ser lo que esperas si estás acostumbrado a un lenguaje con herencia basada en clases.
JavaScript te permite agregar métodos a todas las instancias existentes incluso después de que se crearon.
Aprendimos que cada instancia mantiene una referencia a la propiedad prototype de la función constructora.
Eso significa que si agregas una entrada a ese objeto prototype, esa nueva entrada (por ejemplo, un nuevo método) está 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 incluso se pueden usar para extender 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, se debe evitar 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 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.
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
De manera similar a las declaraciones de funciones y las expresiones de funciones, JavaScript también admite expresiones de clase además de la declaración de clase que se mostró arriba.
Ten en cuenta que, detrás de escena, JavaScript sigue siendo un lenguaje basado en prototipos. Todos los mecanismos que aprendimos en la sección «Sintaxis de prototipos» anterior siguen aplicándose.
Por defecto, todos los campos de instancia son públicos en JavaScript. Se puede acceder a ellos directamente y asignarles valores.
Agregar 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 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.
En la POO, a veces quieres proporcionar campos o métodos de utilidad que no dependen de la instancia específica.
En cambio, se definen para la clase en sí.
Esto se puede lograr 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 cubre en el concepto Herencia.
this Examples - As an object method, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (consultado el 03 de diciembre de 2021) ↩